/**
 * Disk Space Monitor
 *
 * Tracks Docker + system disk usage against a user-configured budget.
 * When usage exceeds thresholds, triggers automatic cleanup and notifications.
 *
 * Key concepts:
 *   - diskBudgetGB:  How much disk the user is willing to give DashCaddy (default 10)
 *   - The monitor calculates Docker's footprint (images, volumes, containers, build cache)
 *   - Breakdown shows where space goes so users can make informed decisions
 *   - Auto-cleanup triggers at 80% (warning), 90% (aggressive), 95% (critical)
 */

const EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { promisify } = require('util');

const execFileAsync = promisify(execFile);

const DEFAULT_BUDGET_GB = 10;
const DEFAULT_CONFIG = {
  enabled: true,
  diskBudgetGB: DEFAULT_BUDGET_GB,
  warningThresholdPct: 80,
  criticalThresholdPct: 90,
  autoCleanup: true,
  cleanupAggressivePct: 95,
};

class DiskSpaceMonitor extends EventEmitter {
  constructor({ log, config }) {
    super();
    this.log = log;
    this.config = config;
    this.lastSnapshot = null;
    this.lastCleanup = null;
    this.intervalHandle = null;
    this.diskConfig = { ...DEFAULT_CONFIG };
    this._loadConfig();
  }

  /**
   * Load disk budget config from the site config file
   * Stored under `diskSpace` key in config.json
   */
  _loadConfig() {
    try {
      const raw = this.config?.diskSpace;
      if (raw) {
        this.diskConfig = {
          ...DEFAULT_CONFIG,
          ...raw,
        };
      }
    } catch {
      // Use defaults
    }
  }

  /**
   * Update disk space settings
   */
  configure(updates) {
    const prev = { ...this.diskConfig };
    this.diskConfig = { ...this.diskConfig, ...updates };
    this._persistConfig();
    this.emit('config-changed', { prev, current: this.diskConfig });
    return this.diskConfig;
  }

  _persistConfig() {
    // The config is persisted by the caller (settings route) which merges
    // into config.json. We just expose the current state.
    if (this.config) {
      this.config.diskSpace = this.diskConfig;
    }
  }

  /**
   * Get a disk usage snapshot using `df` and `docker system df -v`
   */
  async getSnapshot() {
    const [diskInfo, dockerInfo] = await Promise.all([
      this._getDiskInfo(),
      this._getDockerInfo(),
    ]);

    const snapshot = {
      timestamp: new Date().toISOString(),
      system: diskInfo,
      docker: dockerInfo,
      budget: {
        configuredGB: this.diskConfig.diskBudgetGB,
        dockerUsageGB: dockerInfo.totalGB,
        remainingBudgetGB: Math.max(0, this.diskConfig.diskBudgetGB - dockerInfo.totalGB),
        budgetUsedPct: Math.min(100, Math.round((dockerInfo.totalGB / this.diskConfig.diskBudgetGB) * 100)),
        status: this._getBudgetStatus(dockerInfo.totalGB),
      },
      config: { ...this.diskConfig },
      lastCleanup: this.lastCleanup,
    };

    this.lastSnapshot = snapshot;

    // Check thresholds and emit events
    this._checkThresholds(snapshot);

    return snapshot;
  }

  _getBudgetStatus(dockerUsageGB) {
    const pct = (dockerUsageGB / this.diskConfig.diskBudgetGB) * 100;
    if (pct >= this.diskConfig.cleanupAggressivePct) return 'critical';
    if (pct >= this.diskConfig.criticalThresholdPct) return 'aggressive';
    if (pct >= this.diskConfig.warningThresholdPct) return 'warning';
    return 'healthy';
  }

  _checkThresholds(snapshot) {
    const { status, budgetUsedPct } = snapshot.budget;
    if (status === 'critical' || status === 'aggressive') {
      this.emit('budget-exceeded', snapshot);
      if (this.diskConfig.autoCleanup) {
        this.performCleanup(status === 'critical' ? 'aggressive' : 'standard').catch(() => {});
      }
    } else if (status === 'warning') {
      this.emit('budget-warning', snapshot);
    }
  }

  async _getDiskInfo() {
    try {
      const { stdout } = await execFileAsync('df', ['-B1', '/']);
      const lines = stdout.trim().split('\n');
      const parts = lines[1].split(/\s+/);
      return {
        totalBytes: parseInt(parts[1], 10),
        usedBytes: parseInt(parts[2], 10),
        availableBytes: parseInt(parts[3], 10),
        usedPct: parseInt(parts[4], 10),
        mount: parts[5],
        totalGB: Math.round(parseInt(parts[1], 10) / 1073741824 * 10) / 10,
        usedGB: Math.round(parseInt(parts[2], 10) / 1073741824 * 10) / 10,
        availableGB: Math.round(parseInt(parts[3], 10) / 1073741824 * 10) / 10,
      };
    } catch {
      return { totalBytes: 0, usedBytes: 0, availableBytes: 0, usedPct: 0, totalGB: 0, usedGB: 0, availableGB: 0 };
    }
  }

  async _getDockerInfo() {
    try {
      const { stdout } = await execFileAsync('docker', ['system', 'df', '--format', '{{json .}}']);
      const lines = stdout.trim().split('\n').filter(Boolean);

      let images = { count: 0, totalGB: 0, reclaimableGB: 0 };
      let containers = { count: 0, totalGB: 0, reclaimableGB: 0 };
      let volumes = { count: 0, totalGB: 0, reclaimableGB: 0 };
      let buildCache = { count: 0, totalGB: 0, reclaimableGB: 0 };

      for (const line of lines) {
        try {
          const d = JSON.parse(line);
          const type = d.Type?.toLowerCase() || '';
          const sizeGB = this._parseSizeToGB(d.Size);
          const reclaimGB = this._parseSizeToGB(d.Reclaimable);

          if (type === 'images') images = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
          else if (type === 'containers') containers = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
          else if (type === 'local volumes') volumes = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
          else if (type === 'build cache') buildCache = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
        } catch { /* skip unparseable lines */ }
      }

      const totalGB = Math.round((images.totalGB + containers.totalGB + volumes.totalGB + buildCache.totalGB) * 100) / 100;
      const reclaimableGB = Math.round((images.reclaimableGB + containers.reclaimableGB + volumes.reclaimableGB + buildCache.reclaimableGB) * 100) / 100;

      return {
        images,
        containers,
        volumes,
        buildCache,
        totalGB,
        reclaimableGB,
      };
    } catch {
      return { images: {}, containers: {}, volumes: {}, buildCache: {}, totalGB: 0, reclaimableGB: 0 };
    }
  }

  /**
   * Parse Docker's human-readable size strings (e.g., "2.519GB", "8.108MB", "0B")
   */
  _parseSizeToGB(str) {
    if (!str || str === '0B') return 0;
    const match = str.match(/^([\d.]+)(B|KB|MB|GB|TB)$/i);
    if (!match) return 0;
    const value = parseFloat(match[1]);
    const unit = match[2].toUpperCase();
    const multipliers = { B: 1e-9, KB: 1e-6, MB: 1e-3, GB: 1, TB: 1e3 };
    return Math.round(value * (multipliers[unit] || 0) * 1000) / 1000;
  }

  /**
   * Get per-container log file sizes (the hidden disk hog)
   */
  async _getContainerLogs() {
    try {
      const { stdout } = await execFileAsync('sh', ['-c', 'for f in /var/lib/docker/containers/*/*-json.log; do [ -f "$f" ] && stat -c "%s %n" "$f"; done 2>/dev/null | sort -rn | head -10']);
      const entries = [];
      for (const line of stdout.trim().split('\n').filter(Boolean)) {
        const [sizeStr, ...fileParts] = line.split(' ');
        const sizeBytes = parseInt(sizeStr, 10);
        entries.push({
          sizeBytes,
          sizeMB: Math.round(sizeBytes / 1048576 * 10) / 10,
          file: fileParts.join(' '),
        });
      }
      return entries;
    } catch {
      return [];
    }
  }

  /**
   * Perform cleanup
   * @param {string} level - 'standard' | 'aggressive' | 'logs-only'
   * @returns {Object} cleanup result with bytes reclaimed
   */
  async performCleanup(level = 'standard') {
    const startTime = Date.now();
    const result = {
      level,
      startedAt: new Date(startTime).toISOString(),
      actions: [],
      bytesReclaimed: 0,
    };

    try {
      // Always: truncate oversized container logs
      const logsBefore = await this._getContainerLogs();
      let logBytesFreed = 0;
      for (const log of logsBefore) {
        if (log.sizeBytes > 100 * 1048576) { // > 100MB
          try {
            await execFileAsync('truncate', ['-s', '0', log.file]);
            logBytesFreed += log.sizeBytes;
            result.actions.push({ action: 'truncate-log', file: log.file, freedBytes: log.sizeBytes });
          } catch { /* skip */ }
        }
      }
      result.bytesReclaimed += logBytesFreed;

      // Always: vacuum journald to 200MB
      try {
        const { stdout } = await execFileAsync('journalctl', ['--vacuum-size=200M']);
        const freedMatch = stdout.match(/freed ([\d.]+[KMGT]?B)/i);
        if (freedMatch) {
          const freedBytes = this._humanToBytes(freedMatch[1]);
          result.bytesReclaimed += freedBytes;
          result.actions.push({ action: 'vacuum-journal', freedBytes, freedHuman: freedMatch[1] });
        }
      } catch { /* skip */ }

      if (level === 'standard' || level === 'aggressive') {
        // Prune dangling images
        try {
          const { stdout } = await execFileAsync('docker', ['image', 'prune', '-f', '--filter', 'dangling=true']);
          const reclaimed = this._extractDockerReclaimed(stdout);
          result.bytesReclaimed += reclaimed;
          result.actions.push({ action: 'prune-dangling-images', freedBytes: reclaimed });
        } catch { /* skip */ }

        // Prune unused volumes
        try {
          const { stdout } = await execFileAsync('docker', ['volume', 'prune', '-f']);
          const reclaimed = this._extractDockerReclaimed(stdout);
          result.bytesReclaimed += reclaimed;
          result.actions.push({ action: 'prune-unused-volumes', freedBytes: reclaimed });
        } catch { /* skip */ }

        // Prune build cache (keep last 500MB)
        try {
          const { stdout } = await execFileAsync('docker', ['builder', 'prune', '-f', '--keep-storage', '500m']);
          const reclaimed = this._extractDockerReclaimed(stdout);
          result.bytesReclaimed += reclaimed;
          result.actions.push({ action: 'prune-build-cache', freedBytes: reclaimed });
        } catch { /* skip */ }
      }

      if (level === 'aggressive') {
        // Remove ALL images not used by running containers
        try {
          const { stdout } = await execFileAsync('docker', ['image', 'prune', '-a', '-f']);
          const reclaimed = this._extractDockerReclaimed(stdout);
          result.bytesReclaimed += reclaimed;
          result.actions.push({ action: 'prune-all-unused-images', freedBytes: reclaimed });
        } catch { /* skip */ }

        // Prune stopped containers older than 24h
        try {
          const { stdout } = await execFileAsync('docker', ['container', 'prune', '-f', '--filter', 'until=24h']);
          const reclaimed = this._extractDockerReclaimed(stdout);
          result.bytesReclaimed += reclaimed;
          result.actions.push({ action: 'prune-old-containers', freedBytes: reclaimed });
        } catch { /* skip */ }
      }

      result.completedAt = new Date().toISOString();
      result.durationMs = Date.now() - startTime;
      result.bytesReclaimedGB = Math.round(result.bytesReclaimed / 1073741824 * 100) / 100;

      this.lastCleanup = result;
      this.emit('cleanup-complete', result);

      if (this.log) {
        this.log.info('disk', 'Disk cleanup completed', {
          level,
          bytesReclaimed: result.bytesReclaimed,
          GBReclaimed: result.bytesReclaimedGB,
          durationMs: result.durationMs,
          actions: result.actions.length,
        });
      }

      return result;
    } catch (err) {
      result.error = err.message;
      result.completedAt = new Date().toISOString();
      if (this.log) {
        this.log.error('disk', err, null, { note: 'Disk cleanup failed', level });
      }
      return result;
    }
  }

  _humanToBytes(str) {
    const match = str.match(/^([\d.]+)(B|KB|MB|GB|TB)$/i);
    if (!match) return 0;
    const value = parseFloat(match[1]);
    const unit = match[2].toUpperCase();
    const multipliers = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824, TB: 1099511627776 };
    return Math.round(value * (multipliers[unit] || 0));
  }

  _extractDockerReclaimed(stdout) {
    const match = stdout.match(/reclaimed\s+([\d.]+[KMGT]?B)/i) || stdout.match(/Total reclaimed space:\s*([\d.]+[KMGT]?B)/i);
    if (match) return this._humanToBytes(match[1]);
    return 0;
  }

  /**
   * Start periodic monitoring
   * @param {number} intervalMs - check interval (default 10 minutes)
   */
  start(intervalMs = 600000) {
    if (this.intervalHandle) return;
    this.log?.info?.('disk', 'Disk space monitor started', { intervalMs });
    // Initial check
    this.getSnapshot().catch(() => {});
    this.intervalHandle = setInterval(() => {
      this.getSnapshot().catch(() => {});
    }, intervalMs);
  }

  stop() {
    if (this.intervalHandle) {
      clearInterval(this.intervalHandle);
      this.intervalHandle = null;
    }
  }

  getConfig() {
    return { ...this.diskConfig };
  }

  async getDetailedBreakdown() {
    const [snapshot, containerLogs] = await Promise.all([
      this.getSnapshot(),
      this._getContainerLogs(),
    ]);
    return {
      ...snapshot,
      containerLogs,
    };
  }
}

module.exports = { DiskSpaceMonitor, DEFAULT_DISK_CONFIG: DEFAULT_CONFIG };
