import { logger } from './logger.js';
import { CertBackoffDoc } from './db/index.js';

interface IBackoffEntry {
  failures: number;
  lastFailure: string; // ISO string
  retryAfter: string;  // ISO string
  lastError?: string;
}

/**
 * Manages certificate provisioning scheduling with:
 * - Per-domain exponential backoff persisted via CertBackoffDoc
 *
 * Note: Serial stagger queue was removed — smartacme v9 handles
 * concurrency, per-domain dedup, and rate limiting internally.
 */
export class CertProvisionScheduler {
  private maxBackoffHours: number;

  // In-memory backoff cache (mirrors storage for fast lookups)
  private backoffCache = new Map<string, IBackoffEntry>();

  constructor(
    options?: { maxBackoffHours?: number }
  ) {
    this.maxBackoffHours = options?.maxBackoffHours ?? 24;
  }

  /**
   * Sanitized domain key for storage lookups
   */
  private sanitizeDomain(domain: string): string {
    return domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
  }

  /**
   * Load backoff entry from database (with in-memory cache)
   */
  private async loadBackoff(domain: string): Promise<IBackoffEntry | null> {
    const cached = this.backoffCache.get(domain);
    if (cached) return cached;

    const sanitized = this.sanitizeDomain(domain);
    const doc = await CertBackoffDoc.findByDomain(sanitized);
    if (doc) {
      const entry: IBackoffEntry = {
        failures: doc.failures,
        lastFailure: doc.lastFailure,
        retryAfter: doc.retryAfter,
        lastError: doc.lastError,
      };
      this.backoffCache.set(domain, entry);
      return entry;
    }
    return null;
  }

  /**
   * Save backoff entry to both cache and database
   */
  private async saveBackoff(domain: string, entry: IBackoffEntry): Promise<void> {
    this.backoffCache.set(domain, entry);
    const sanitized = this.sanitizeDomain(domain);
    let doc = await CertBackoffDoc.findByDomain(sanitized);
    if (!doc) {
      doc = new CertBackoffDoc();
      doc.domain = sanitized;
    }
    doc.failures = entry.failures;
    doc.lastFailure = entry.lastFailure;
    doc.retryAfter = entry.retryAfter;
    doc.lastError = entry.lastError || '';
    await doc.save();
  }

  /**
   * Check if a domain is currently in backoff.
   * Expired entries are pruned from the cache to prevent unbounded growth.
   */
  async isInBackoff(domain: string): Promise<boolean> {
    const entry = await this.loadBackoff(domain);
    if (!entry) return false;

    const retryAfter = new Date(entry.retryAfter);
    if (retryAfter.getTime() > Date.now()) {
      return true;
    }

    // Backoff has expired — prune the stale entry
    this.backoffCache.delete(domain);
    return false;
  }

  /**
   * Record a provisioning failure for a domain.
   * Sets exponential backoff: min(failures^2 * 1h, maxBackoffHours)
   */
  async recordFailure(domain: string, error?: string): Promise<void> {
    const existing = await this.loadBackoff(domain);
    const failures = (existing?.failures ?? 0) + 1;

    // Exponential backoff: failures^2 hours, capped
    const backoffHours = Math.min(failures * failures, this.maxBackoffHours);
    const retryAfter = new Date(Date.now() + backoffHours * 60 * 60 * 1000);

    const entry: IBackoffEntry = {
      failures,
      lastFailure: new Date().toISOString(),
      retryAfter: retryAfter.toISOString(),
      lastError: error,
    };

    await this.saveBackoff(domain, entry);
    logger.log('warn', `Cert backoff for ${domain}: ${failures} failures, retry after ${retryAfter.toISOString()}`);
  }

  /**
   * Clear backoff for a domain (on success or manual override)
   */
  async clearBackoff(domain: string): Promise<void> {
    this.backoffCache.delete(domain);
    try {
      const sanitized = this.sanitizeDomain(domain);
      const doc = await CertBackoffDoc.findByDomain(sanitized);
      if (doc) {
        await doc.delete();
      }
    } catch {
      // Ignore delete errors (doc may not exist)
    }
  }

  /**
   * Clear all in-memory backoff cache entries
   */
  public clear(): void {
    this.backoffCache.clear();
  }

  /**
   * Get backoff info for UI display
   */
  async getBackoffInfo(domain: string): Promise<{
    failures: number;
    retryAfter?: string;
    lastError?: string;
  } | null> {
    const entry = await this.loadBackoff(domain);
    if (!entry) return null;

    // Only return if still in backoff — prune expired entries
    const retryAfter = new Date(entry.retryAfter);
    if (retryAfter.getTime() <= Date.now()) {
      this.backoffCache.delete(domain);
      return null;
    }

    return {
      failures: entry.failures,
      retryAfter: entry.retryAfter,
      lastError: entry.lastError,
    };
  }
}
