/**
 * Config Drift Detector - Compares services.json with live Docker state
 *
 * Detects discrepancies between the configured service list and what is
 * actually running in Docker, including missing containers, unknown
 * containers, port mismatches, state mismatches, and stale records.
 *
 * @module config-drift-detector
 */

const EventEmitter = require('events');

/**
 * @typedef {Object} DriftReport
 * @property {string}            checkedAt          - ISO timestamp of the check
 * @property {Object[]}          missingContainers  - Services with containerId but container absent in Docker
 * @property {Object[]}          unknownContainers  - Running Docker containers with sami.managed label but not in services.json
 * @property {Object[]}          portMismatch       - Service port != container mapped port
 * @property {Object[]}          stateMismatch      - Service expected up but container stopped/absent
 * @property {Object[]}          staleRecords       - Services with containerId pointing to removed containers
 * @property {boolean}           hasDrift           - Whether any drift category is non-empty
 */

/**
 * Detects and reports configuration drift between services.json and Docker.
 *
 * @extends EventEmitter
 *
 * @fires ConfigDriftDetector#drift-detected
 */
class ConfigDriftDetector extends EventEmitter {
  /**
   * @param {Object} ctx - Shared application context
   * @param {Object}   ctx.docker                    - Docker client wrapper ({ client: Dockerode })
   * @param {Object}   ctx.servicesStateManager      - StateManager for services.json
   * @param {Object}   ctx.notification              - NotificationManager instance
   * @param {Object}   ctx.log                       - Logger instance
   * @param {Function} ctx.logError                  - Error logging function
   */
  constructor(ctx) {
    super();
    this.ctx = ctx;
    this.log = ctx.log || console;
    this.logError = ctx.logError || ((_c, err) => console.error(err));
    this.docker = ctx.docker;
    this.servicesStateManager = ctx.servicesStateManager;
    this.notification = ctx.notification;

    /** @type {DriftReport|null} Cached report from last detection */
    this.lastReport = null;

    /** @type {NodeJS.Timeout|null} Polling timer reference */
    this._pollTimer = null;

    /** Whether polling is currently active */
    this._polling = false;
  }

  // ─── Detection ───────────────────────────────────────────────────────

  /**
   * Run a full drift detection and return the report.
   *
   * Reads services from servicesStateManager and live containers from Docker,
   * then compares them across five drift categories.
   *
   * @returns {Promise<DriftReport>}
   */
  async detect() {
    const checkedAt = new Date().toISOString();

    // Gather configured services
    let services = [];
    try {
      const data = await this.servicesStateManager.read();
      services = Array.isArray(data) ? data : (data.services || []);
    } catch (err) {
      this.log.error('drift', 'Failed to read services', { error: err.message });
    }

    // Gather live Docker containers
    let containers = [];
    try {
      containers = await this.docker.client.listContainers({ all: true });
    } catch (err) {
      this.log.error('drift', 'Failed to list containers', { error: err.message });
    }

    // Build lookup maps
    const containerById = new Map();   // containerId (short or long) → container info
    const containerByName = new Map(); // container name → container info

    for (const c of containers) {
      // Store by full ID
      containerById.set(c.Id, c);
      // Store by short ID (first 12 chars)
      if (c.Id && c.Id.length >= 12) {
        containerById.set(c.Id.substring(0, 12), c);
      }
      // Store by name (strip leading /)
      for (const name of (c.Names || [])) {
        containerByName.set(name.replace(/^\//, ''), c);
      }
    }

    // Build set of service containerIds for reverse lookup
    const serviceContainerIds = new Set();
    const serviceByContainerId = new Map();

    for (const svc of services) {
      if (svc.containerId) {
        serviceContainerIds.add(svc.containerId);
        // Index by both full and short ID
        serviceByContainerId.set(svc.containerId, svc);
        if (svc.containerId.length >= 12) {
          serviceByContainerId.set(svc.containerId.substring(0, 12), svc);
        }
      }
    }

    const missingContainers = [];
    const portMismatch = [];
    const stateMismatch = [];
    const staleRecords = [];

    for (const svc of services) {
      if (!svc.containerId) continue;

      // Look up the container
      const container = containerById.get(svc.containerId)
        || containerById.get(svc.containerId.substring(0, 12));

      if (!container) {
        // Container ID referenced but not found in Docker at all
        staleRecords.push({
          serviceId: svc.id,
          name: svc.name,
          containerId: svc.containerId,
          reason: 'Container not found in Docker',
        });
        continue;
      }

      // Missing container — service expects it but it's not running
      if (container.State !== 'running') {
        missingContainers.push({
          serviceId: svc.id,
          name: svc.name,
          containerId: svc.containerId,
          containerState: container.State,
          containerStatus: container.Status,
        });

        // Also a state mismatch if the service is expected to be up
        stateMismatch.push({
          serviceId: svc.id,
          name: svc.name,
          expectedState: 'running',
          actualState: container.State,
          containerId: svc.containerId,
        });
      }

      // Port mismatch detection
      if (svc.port && container.State === 'running') {
        const actualPorts = this._extractContainerPorts(container);
        if (actualPorts.length > 0 && !actualPorts.includes(svc.port)) {
          portMismatch.push({
            serviceId: svc.id,
            name: svc.name,
            configuredPort: svc.port,
            actualPorts,
            containerId: svc.containerId,
          });
        }
      }
    }

    // Unknown managed containers: Docker containers with sami.managed label
    // that are NOT in services.json
    const unknownContainers = [];
    for (const c of containers) {
      const isManaged = c.Labels && c.Labels['sami.managed'] === 'true';
      if (!isManaged) continue;

      const isInServices = serviceByContainerId.has(c.Id)
        || serviceByContainerId.has(c.Id.substring(0, 12));

      if (!isInServices) {
        unknownContainers.push({
          containerId: c.Id,
          name: (c.Names && c.Names[0] || '').replace(/^\//, ''),
          image: c.Image,
          state: c.State,
          status: c.Status,
          app: c.Labels?.['sami.app'] || null,
          subdomain: c.Labels?.['sami.subdomain'] || null,
        });
      }
    }

    const report = {
      checkedAt,
      missingContainers,
      unknownContainers,
      portMismatch,
      stateMismatch,
      staleRecords,
      hasDrift: missingContainers.length > 0
        || unknownContainers.length > 0
        || portMismatch.length > 0
        || stateMismatch.length > 0
        || staleRecords.length > 0,
    };

    // Cache for quick API access
    this.lastReport = report;

    // Emit and notify if drift detected
    if (report.hasDrift) {
      /**
       * @event ConfigDriftDetector#drift-detected
       * @type {DriftReport}
       */
      this.emit('drift-detected', report);

      try {
        await this._sendDriftNotification(report);
      } catch (notifErr) {
        this.log.error('drift', 'Failed to send drift notification', {
          error: notifErr.message,
        });
      }
    }

    this.log.info('drift', 'Detection complete', {
      hasDrift: report.hasDrift,
      missing: report.missingContainers.length,
      unknown: report.unknownContainers.length,
      portMismatch: report.portMismatch.length,
      stateMismatch: report.stateMismatch.length,
      stale: report.staleRecords.length,
    });

    return report;
  }

  // ─── Auto-fix ────────────────────────────────────────────────────────

  /**
   * Attempt to auto-fix drift:
   *  - Remove stale records (services referencing removed containers)
   *  - Flag unknown containers for review
   *
   * @returns {Promise<{ staleRemoved: number, unknownFlagged: number }>}
   */
  async autoFix() {
    const report = await this.detect();
    let staleRemoved = 0;

    // Remove stale records from services.json
    if (report.staleRecords.length > 0) {
      const staleIds = new Set(report.staleRecords.map(r => r.serviceId));
      await this.servicesStateManager.update(services => {
        const before = services.length;
        const cleaned = services.filter(s => !staleIds.has(s.id));
        staleRemoved = before - cleaned.length;
        return cleaned;
      });
    }

    const unknownFlagged = report.unknownContainers.length;

    this.log.info('drift', 'Auto-fix applied', { staleRemoved, unknownFlagged });

    return { staleRemoved, unknownFlagged };
  }

  // ─── Polling ─────────────────────────────────────────────────────────

  /**
   * Start periodic drift detection.
   *
   * @param {number} [intervalMs=300000] - Polling interval in milliseconds (default 5 min)
   */
  startPolling(intervalMs = 300000) {
    this.stopPolling();

    this._polling = true;
    this._pollTimer = setInterval(async () => {
      try {
        await this.detect();
      } catch (err) {
        this.logError('drift-poll', err);
      }
    }, intervalMs);

    this.log.info('drift', 'Polling started', { intervalMs });
  }

  /**
   * Stop periodic drift detection.
   */
  stopPolling() {
    if (this._pollTimer) {
      clearInterval(this._pollTimer);
      this._pollTimer = null;
    }
    this._polling = false;
    this.log.info('drift', 'Polling stopped');
  }

  /**
   * Whether polling is currently active.
   * @returns {boolean}
   */
  isPolling() {
    return this._polling;
  }

  // ─── Helpers ─────────────────────────────────────────────────────────

  /**
   * Extract mapped host ports from a Docker container info object.
   *
   * @param {Object} container - Dockerode container info
   * @returns {number[]} Array of host port numbers
   * @private
   */
  _extractContainerPorts(container) {
    const ports = [];
    if (!container.Ports) return ports;

    for (const p of container.Ports) {
      if (p.PublicPort) {
        ports.push(p.PublicPort);
      }
    }

    return ports;
  }

  /**
   * Send a notification about detected drift.
   *
   * @param {DriftReport} report
   * @returns {Promise<Object>}
   * @private
   */
  async _sendDriftNotification(report) {
    if (!this.notification?.send) {
      return { success: false, reason: 'no-notification-manager' };
    }

    const parts = [];
    if (report.missingContainers.length > 0) {
      parts.push(`Missing containers: ${report.missingContainers.map(c => c.name).join(', ')}`);
    }
    if (report.unknownContainers.length > 0) {
      parts.push(`Unknown managed containers: ${report.unknownContainers.map(c => c.name).join(', ')}`);
    }
    if (report.portMismatch.length > 0) {
      parts.push(`Port mismatches: ${report.portMismatch.map(c => c.name).join(', ')}`);
    }
    if (report.staleRecords.length > 0) {
      parts.push(`Stale records: ${report.staleRecords.map(c => c.name).join(', ')}`);
    }

    return this.notification.send('drift-detected', {
      text: `⚠️ Configuration drift detected:\n${parts.join('\n')}`,
      report,
    });
  }
}

module.exports = { ConfigDriftDetector };
