/**
 * SSL Certificate Monitor
 * Periodically checks SSL certificates on services with HTTPS URLs.
 * Alerts at 30, 14, and 7 days before expiry.
 *
 * @module ssl-monitor
 */

const tls = require('tls');
const EventEmitter = require('events');
const path = require('path');
const { readJsonFile, writeJsonFile } = require('../utilities/fs-helpers');
const { resolveServiceUrl } = require('../utilities/url-resolver');

/** Default check interval: 1 hour */
const DEFAULT_INTERVAL_MS = 3600000;

/** Alert thresholds in days */
const THRESHOLDS = {
  WARNING: 30,
  URGENT: 14,
  CRITICAL: 7
};

/** TLS connection timeout in milliseconds */
const TLS_TIMEOUT_MS = 10000;

class SSLMonitor extends EventEmitter {
  /**
   * Create an SSLMonitor instance.
   * @param {Object} ctx - Shared application context
   * @param {Object} ctx.servicesStateManager - State manager for reading services
   * @param {Function} ctx.buildServiceUrl - URL builder helper
   * @param {Object} ctx.siteConfig - Site configuration
   * @param {Object} ctx.notification - NotificationManager instance
   * @param {Object} ctx.log - Logger instance
   * @param {string} [ctx.SSL_CACHE_FILE] - Path to persist SSL cache
   */
  constructor(ctx) {
    super();
    this.ctx = ctx;
    this.log = ctx.log || console;

    /** @type {Map<string, Object>} hostname → last cert check result */
    this.certStatus = new Map();

    /** @type {Map<string, number>} hostname → last notified threshold level */
    this.notifiedThresholds = new Map();

    /** @type {Map<string, string>} hostname → service ID mapping */
    this.hostnameToServiceId = new Map();

    /** @type {NodeJS.Timeout|null} */
    this.intervalHandle = null;

    /** Current config */
    this.config = {
      enabled: true,
      intervalMs: DEFAULT_INTERVAL_MS
    };

    /** Cache file path */
    this.cacheFile = ctx.SSL_CACHE_FILE ||
      path.join(path.dirname(ctx.SERVICES_FILE || './data'), 'ssl-cache.json');
  }

  /**
   * Check the SSL certificate for a given hostname and port.
   * Connects via TLS with rejectUnauthorized: false to retrieve certificate info.
   *
   * @param {string} hostname - The hostname to check
   * @param {number} [port=443] - The port to connect to
   * @returns {Promise<Object>} Certificate information
   */
  async checkCert(hostname, port = 443) {
    return new Promise((resolve, reject) => {
      const socket = tls.connect({
        host: hostname,
        port,
        rejectUnauthorized: false,
        servername: hostname,
        timeout: TLS_TIMEOUT_MS
      }, () => {
        try {
          const cert = socket.getPeerCertificate();

          if (!cert || Object.keys(cert).length === 0) {
            socket.destroy();
            return reject(new Error(`No certificate returned for ${hostname}:${port}`));
          }

          const validFrom = new Date(cert.valid_from);
          const validTo = new Date(cert.valid_to);
          const now = new Date();
          const msRemaining = validTo.getTime() - now.getTime();
          const daysRemaining = Math.ceil(msRemaining / (1000 * 60 * 60 * 24));

          const result = {
            hostname,
            port,
            subject: cert.subject?.CN || cert.subject?.O || 'Unknown',
            issuer: cert.issuer?.CN || cert.issuer?.O || 'Unknown',
            validFrom: cert.valid_from,
            validTo: cert.valid_to,
            daysRemaining,
            fingerprint: cert.fingerprint || null,
            isExpiring: daysRemaining <= THRESHOLDS.WARNING,
            checkedAt: new Date().toISOString()
          };

          socket.destroy();
          resolve(result);
        } catch (err) {
          socket.destroy();
          reject(err);
        }
      });

      socket.on('error', (err) => {
        reject(new Error(`TLS connect error for ${hostname}:${port}: ${err.message}`));
      });

      socket.setTimeout(TLS_TIMEOUT_MS, () => {
        socket.destroy(new Error(`TLS connection timeout for ${hostname}:${port}`));
        reject(new Error(`TLS connection timeout for ${hostname}:${port}`));
      });
    });
  }

  /**
   * Check SSL certificates for all services that have HTTPS URLs.
   * Reads services from ctx.servicesStateManager, resolves URLs, and checks each HTTPS cert.
   *
   * @returns {Promise<Object>} Map of hostname → cert status
   */
  async checkAll() {
    if (!this.config.enabled) {
      this.log.info('ssl-monitor', 'SSL monitoring is disabled, skipping check');
      return this.getStatus();
    }

    let servicesData;
    try {
      servicesData = await this.ctx.servicesStateManager.read();
    } catch (err) {
      this.log.error('ssl-monitor', 'Failed to read services', { error: err.message });
      return this.getStatus();
    }

    const services = Array.isArray(servicesData) ? servicesData : (servicesData.services || []);

    for (const service of services) {
      const serviceId = service.id || service.name?.toLowerCase();
      if (!serviceId) continue;

      try {
        const url = resolveServiceUrl(serviceId, service, this.ctx.siteConfig, this.ctx.buildServiceUrl);
        if (!url) continue;

        const parsed = new URL(url);
        if (parsed.protocol !== 'https:') continue;

        const hostname = parsed.hostname;
        const port = parseInt(parsed.port) || 443;

        // Map hostname back to service ID
        this.hostnameToServiceId.set(hostname, serviceId);

        const result = await this.checkCert(hostname, port);

        // Store result
        this.certStatus.set(hostname, result);

        // Emit check event
        this.emit('cert-check', { serviceId, hostname, result });

        // Check alert thresholds
        await this._checkAndNotify(hostname, result, serviceId);
      } catch (err) {
        this.log.warn('ssl-monitor', `Failed to check cert for service ${serviceId}`, {
          error: err.message
        });
      }
    }

    // Persist results
    await this._saveCache();

    return this.getStatus();
  }

  /**
   * Start periodic SSL certificate checking.
   *
   * @param {number} [intervalMs=3600000] - Check interval in milliseconds
   */
  start(intervalMs) {
    if (intervalMs !== undefined) {
      this.config.intervalMs = intervalMs;
    }
    if (this.intervalHandle) {
      this.log.warn('ssl-monitor', 'SSL monitor is already running');
      return;
    }

    this.config.enabled = true;

    // Load cached data
    this._loadCache().catch(err => {
      this.log.warn('ssl-monitor', 'Failed to load SSL cache', { error: err.message });
    });

    // Initial check (non-blocking)
    this.checkAll().catch(err => {
      this.log.error('ssl-monitor', 'Initial SSL check failed', { error: err.message });
    });

    // Schedule periodic checks
    this.intervalHandle = setInterval(() => {
      this.checkAll().catch(err => {
        this.log.error('ssl-monitor', 'Periodic SSL check failed', { error: err.message });
      });
    }, this.config.intervalMs);

    this.log.info('ssl-monitor', 'SSL monitoring started', {
      intervalMs: this.config.intervalMs
    });
  }

  /**
   * Stop periodic SSL certificate checking.
   */
  stop() {
    if (this.intervalHandle) {
      clearInterval(this.intervalHandle);
      this.intervalHandle = null;
    }
    this.config.enabled = false;
    this.log.info('ssl-monitor', 'SSL monitoring stopped');
  }

  /**
   * Get the current SSL certificate status for all checked hostnames.
   *
   * @returns {Object} Map of hostname → cert status
   */
  getStatus() {
    const status = {};
    for (const [hostname, cert] of this.certStatus.entries()) {
      status[hostname] = { ...cert };
    }
    return status;
  }

  /**
   * Get the SSL certificate status for a specific service.
   *
   * @param {string} serviceId - The service ID to look up
   * @returns {Object|null} Certificate status or null if not found
   */
  getServiceCertStatus(serviceId) {
    // Find hostname mapped to this service
    for (const [hostname, id] of this.hostnameToServiceId.entries()) {
      if (id === serviceId) {
        const cert = this.certStatus.get(hostname);
        return cert ? { ...cert, serviceId } : null;
      }
    }
    return null;
  }

  /**
   * Get current monitoring configuration.
   *
   * @returns {Object} Config with interval and enabled state
   */
  getConfig() {
    return { ...this.config };
  }

  /**
   * Update monitoring configuration.
   *
   * @param {Object} updates - Config updates
   * @param {boolean} [updates.enabled] - Enable/disable monitoring
   * @param {number} [updates.intervalMs] - Check interval in milliseconds
   */
  updateConfig(updates) {
    if (typeof updates.enabled === 'boolean') {
      this.config.enabled = updates.enabled;
      if (!updates.enabled && this.intervalHandle) {
        this.stop();
      }
    }
    if (typeof updates.intervalMs === 'number' && updates.intervalMs >= 60000) {
      this.config.intervalMs = updates.intervalMs;
      // Restart interval if running
      if (this.intervalHandle) {
        clearInterval(this.intervalHandle);
        this.intervalHandle = setInterval(() => {
          this.checkAll().catch(err => {
            this.log.error('ssl-monitor', 'Periodic SSL check failed', { error: err.message });
          });
        }, this.config.intervalMs);
      }
    }
  }

  // ===== Private Methods =====

  /**
   * Check alert thresholds and send notifications if thresholds are crossed.
   * Only sends one notification per threshold per hostname.
   *
   * @param {string} hostname
   * @param {Object} certResult
   * @param {string} serviceId
   */
  async _checkAndNotify(hostname, certResult, serviceId) {
    const { daysRemaining } = certResult;
    const key = hostname;
    const lastNotified = this.notifiedThresholds.get(key) || Infinity;

    let level = null;
    let eventType = null;
    let message = null;

    if (daysRemaining <= THRESHOLDS.CRITICAL) {
      level = THRESHOLDS.CRITICAL;
      eventType = 'cert-critical';
      message = `🔒 CRITICAL: SSL certificate for ${hostname} expires in ${daysRemaining} days!`;
    } else if (daysRemaining <= THRESHOLDS.URGENT) {
      level = THRESHOLDS.URGENT;
      eventType = 'cert-expiring';
      message = `⚠️ URGENT: SSL certificate for ${hostname} expires in ${daysRemaining} days`;
    } else if (daysRemaining <= THRESHOLDS.WARNING) {
      level = THRESHOLDS.WARNING;
      eventType = 'cert-expiring';
      message = `⚠️ SSL certificate for ${hostname} expires in ${daysRemaining} days`;
    }

    if (level !== null && level < lastNotified) {
      // New threshold crossed — send notification
      this.notifiedThresholds.set(key, level);
      this.emit(eventType, { hostname, serviceId, daysRemaining, level });

      if (this.ctx.notification) {
        try {
          await this.ctx.notification.send('ssl-cert-expiry', {
            text: message,
            hostname,
            serviceId,
            daysRemaining,
            level,
            validTo: certResult.validTo
          }, level <= THRESHOLDS.CRITICAL ? 'error' : 'warning');
        } catch (err) {
          this.log.error('ssl-monitor', 'Failed to send SSL notification', { error: err.message });
        }
      }
    } else if (level === null) {
      // Cert is healthy — reset notification tracking
      this.notifiedThresholds.delete(key);
    }
  }

  /**
   * Persist cert status cache to disk.
   */
  async _saveCache() {
    try {
      const data = {
        lastChecked: new Date().toISOString(),
        certs: {},
        hostnameToServiceId: Object.fromEntries(this.hostnameToServiceId)
      };
      for (const [hostname, cert] of this.certStatus.entries()) {
        data.certs[hostname] = cert;
      }
      await writeJsonFile(this.cacheFile, data);
    } catch (err) {
      this.log.warn('ssl-monitor', 'Failed to save SSL cache', { error: err.message });
    }
  }

  /**
   * Load cert status cache from disk.
   */
  async _loadCache() {
    try {
      const data = await readJsonFile(this.cacheFile, null);
      if (data && data.certs) {
        for (const [hostname, cert] of Object.entries(data.certs)) {
          this.certStatus.set(hostname, cert);
        }
        if (data.hostnameToServiceId) {
          for (const [hostname, serviceId] of Object.entries(data.hostnameToServiceId)) {
            this.hostnameToServiceId.set(hostname, serviceId);
          }
        }
        this.log.info('ssl-monitor', 'Loaded SSL cache', {
          certCount: this.certStatus.size
        });
      }
    } catch (err) {
      this.log.warn('ssl-monitor', 'Failed to load SSL cache', { error: err.message });
    }
  }
}

module.exports = SSLMonitor;
