const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFile } = require('child_process');
const { promisify } = require('util');
const settingsService = require('./settingsService');
const wsService = require('./websocketService');
const logger = require('./logger').child('HWMON');
const { errorToMeta } = require('../utils/errorMeta');

const execFileAsync = promisify(execFile);

const DEFAULT_INTERVAL_MS = 5000;
const MIN_INTERVAL_MS = 1000;
const MAX_INTERVAL_MS = 60000;
const DF_TIMEOUT_MS = 1800;
const SENSORS_TIMEOUT_MS = 1800;
const NVIDIA_SMI_TIMEOUT_MS = 1800;
const RELEVANT_SETTINGS_KEYS = new Set([
  'hardware_monitoring_enabled',
  'hardware_monitoring_interval_ms',
  'raw_dir',
  'raw_dir_bluray',
  'raw_dir_dvd',
  'raw_dir_cd',
  'movie_dir',
  'movie_dir_bluray',
  'movie_dir_dvd',
  'log_dir',
  'external_storage_paths'
]);

function nowIso() {
  return new Date().toISOString();
}

function toBoolean(value) {
  if (typeof value === 'boolean') {
    return value;
  }
  if (typeof value === 'number') {
    return value !== 0;
  }
  const normalized = String(value || '').trim().toLowerCase();
  if (!normalized) {
    return false;
  }
  if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
    return true;
  }
  if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
    return false;
  }
  return Boolean(normalized);
}

function normalizePathSetting(value) {
  return String(value || '').trim();
}

function parseExternalStoragePaths(rawValue) {
  const normalized = String(rawValue || '').trim();
  if (!normalized) {
    return [];
  }

  let parsed = [];
  try {
    parsed = JSON.parse(normalized);
  } catch (_error) {
    return [];
  }

  const list = Array.isArray(parsed) ? parsed : [];
  const unique = [];
  const seen = new Set();
  for (const item of list) {
    const pathCandidate = typeof item === 'string'
      ? item
      : (item && typeof item === 'object' ? item.path : '');
    const labelCandidate = item && typeof item === 'object'
      ? (item.name ?? item.label ?? '')
      : '';
    const pathValue = normalizePathSetting(pathCandidate);
    const labelValue = normalizePathSetting(labelCandidate);
    if (!pathValue || seen.has(pathValue)) {
      continue;
    }
    seen.add(pathValue);
    unique.push({
      path: pathValue,
      label: labelValue || null
    });
  }
  return unique;
}

function isRootPath(inputPath) {
  const normalized = String(inputPath || '').trim();
  if (!normalized) {
    return false;
  }
  const resolved = path.resolve(normalized);
  return resolved === path.parse(resolved).root;
}

function clampIntervalMs(rawValue) {
  const parsed = Number(rawValue);
  if (!Number.isFinite(parsed)) {
    return DEFAULT_INTERVAL_MS;
  }
  const clamped = Math.max(MIN_INTERVAL_MS, Math.min(MAX_INTERVAL_MS, Math.trunc(parsed)));
  return clamped || DEFAULT_INTERVAL_MS;
}

function roundNumber(rawValue, digits = 1) {
  const value = Number(rawValue);
  if (!Number.isFinite(value)) {
    return null;
  }
  const factor = 10 ** digits;
  return Math.round(value * factor) / factor;
}

function averageNumberList(values = []) {
  const list = (Array.isArray(values) ? values : []).filter((value) => Number.isFinite(Number(value)));
  if (list.length === 0) {
    return null;
  }
  const sum = list.reduce((acc, value) => acc + Number(value), 0);
  return sum / list.length;
}

function parseMaybeNumber(rawValue) {
  if (rawValue === null || rawValue === undefined) {
    return null;
  }
  if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
    return rawValue;
  }
  const normalized = String(rawValue).trim().replace(',', '.');
  if (!normalized) {
    return null;
  }
  const cleaned = normalized.replace(/[^0-9.+-]/g, '');
  if (!cleaned) {
    return null;
  }
  const parsed = Number(cleaned);
  if (!Number.isFinite(parsed)) {
    return null;
  }
  return parsed;
}

function normalizeTempC(rawValue) {
  const parsed = parseMaybeNumber(rawValue);
  if (!Number.isFinite(parsed)) {
    return null;
  }
  let celsius = parsed;
  if (Math.abs(celsius) > 500) {
    celsius = celsius / 1000;
  }
  if (!Number.isFinite(celsius) || celsius <= -40 || celsius >= 160) {
    return null;
  }
  return roundNumber(celsius, 1);
}

function isCommandMissingError(error) {
  return String(error?.code || '').toUpperCase() === 'ENOENT';
}

function readTextFileSafe(filePath) {
  try {
    return fs.readFileSync(filePath, 'utf-8').trim();
  } catch (_error) {
    return '';
  }
}

function collectTemperatureCandidates(node, pathParts = [], out = []) {
  if (!node || typeof node !== 'object') {
    return out;
  }

  for (const [key, value] of Object.entries(node)) {
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      collectTemperatureCandidates(value, [...pathParts, key], out);
      continue;
    }
    if (!/^temp\d+_input$/i.test(String(key || ''))) {
      continue;
    }
    const normalizedTemp = normalizeTempC(value);
    if (normalizedTemp === null) {
      continue;
    }
    out.push({
      label: [...pathParts, key].join(' / '),
      value: normalizedTemp
    });
  }

  return out;
}

function mapTemperatureCandidates(candidates = []) {
  const perCoreSamples = new Map();
  const packageSamples = [];
  const genericSamples = [];

  for (const entry of Array.isArray(candidates) ? candidates : []) {
    const value = Number(entry?.value);
    if (!Number.isFinite(value)) {
      continue;
    }
    const label = String(entry?.label || '');
    const labelLower = label.toLowerCase();
    const coreMatch = labelLower.match(/\bcore\s*([0-9]+)\b/);
    if (coreMatch) {
      const index = Number(coreMatch[1]);
      if (Number.isFinite(index) && index >= 0) {
        const list = perCoreSamples.get(index) || [];
        list.push(value);
        perCoreSamples.set(index, list);
        continue;
      }
    }

    if (/package id|tdie|tctl|cpu package|physical id/.test(labelLower)) {
      packageSamples.push(value);
      continue;
    }

    genericSamples.push(value);
  }

  const perCore = Array.from(perCoreSamples.entries())
    .sort((a, b) => a[0] - b[0])
    .map(([index, values]) => ({
      index,
      temperatureC: roundNumber(averageNumberList(values), 1)
    }))
    .filter((item) => item.temperatureC !== null);

  const overallRaw = packageSamples.length > 0
    ? averageNumberList(packageSamples)
    : (perCore.length > 0 ? averageNumberList(perCore.map((item) => item.temperatureC)) : averageNumberList(genericSamples));
  const overallC = roundNumber(overallRaw, 1);

  return {
    overallC,
    perCore,
    available: Boolean(overallC !== null || perCore.length > 0)
  };
}

function isLikelyCpuTemperatureLabel(label = '') {
  const normalized = String(label || '').trim().toLowerCase();
  if (!normalized) {
    return false;
  }
  return /cpu|core|package|tdie|tctl|physical id|x86_pkg_temp|k10temp|zenpower|cpu-thermal|soc_thermal/.test(normalized);
}

function preferCpuTemperatureCandidates(candidates = []) {
  const list = Array.isArray(candidates) ? candidates : [];
  const cpuLikely = list.filter((item) => isLikelyCpuTemperatureLabel(item?.label));
  return cpuLikely.length > 0 ? cpuLikely : list;
}

function parseDfStats(rawOutput) {
  const lines = String(rawOutput || '')
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean);
  if (lines.length < 2) {
    return null;
  }
  const dataLine = lines[lines.length - 1];
  const columns = dataLine.split(/\s+/);
  if (columns.length < 6) {
    return null;
  }

  const totalKb = parseMaybeNumber(columns[1]);
  const usedKb = parseMaybeNumber(columns[2]);
  const availableKb = parseMaybeNumber(columns[3]);
  const usagePercent = parseMaybeNumber(String(columns[4]).replace('%', ''));
  const mountPoint = columns.slice(5).join(' ');

  if (!Number.isFinite(totalKb) || !Number.isFinite(usedKb) || !Number.isFinite(availableKb)) {
    return null;
  }

  return {
    totalBytes: Math.max(0, Math.round(totalKb * 1024)),
    usedBytes: Math.max(0, Math.round(usedKb * 1024)),
    freeBytes: Math.max(0, Math.round(availableKb * 1024)),
    usagePercent: Number.isFinite(usagePercent)
      ? roundNumber(usagePercent, 1)
      : (totalKb > 0 ? roundNumber((usedKb / totalKb) * 100, 1) : null),
    mountPoint: mountPoint || null
  };
}

function parseNvidiaCsvLine(line) {
  const columns = String(line || '').split(',').map((part) => part.trim());
  if (columns.length < 10) {
    return null;
  }

  const index = parseMaybeNumber(columns[0]);
  const memoryUsedMiB = parseMaybeNumber(columns[5]);
  const memoryTotalMiB = parseMaybeNumber(columns[6]);
  return {
    index: Number.isFinite(index) ? Math.trunc(index) : null,
    name: columns[1] || null,
    utilizationPercent: roundNumber(parseMaybeNumber(columns[2]), 1),
    memoryUtilizationPercent: roundNumber(parseMaybeNumber(columns[3]), 1),
    temperatureC: roundNumber(parseMaybeNumber(columns[4]), 1),
    memoryUsedBytes: Number.isFinite(memoryUsedMiB) ? Math.round(memoryUsedMiB * 1024 * 1024) : null,
    memoryTotalBytes: Number.isFinite(memoryTotalMiB) ? Math.round(memoryTotalMiB * 1024 * 1024) : null,
    powerDrawW: roundNumber(parseMaybeNumber(columns[7]), 1),
    powerLimitW: roundNumber(parseMaybeNumber(columns[8]), 1),
    fanPercent: roundNumber(parseMaybeNumber(columns[9]), 1)
  };
}

class HardwareMonitorService {
  constructor() {
    this.enabled = false;
    this.intervalMs = DEFAULT_INTERVAL_MS;
    this.monitoredPaths = [];
    this.running = false;
    this.timer = null;
    this.pollInFlight = false;
    this.lastCpuTimes = null;
    this.sensorsCommandAvailable = null;
    this.nvidiaSmiAvailable = null;
    this.lastSnapshot = {
      enabled: false,
      intervalMs: DEFAULT_INTERVAL_MS,
      updatedAt: null,
      sample: null,
      error: null
    };
  }

  async init() {
    await this.reloadFromSettings({
      forceBroadcast: true,
      forceImmediatePoll: true
    });
  }

  stop() {
    this.stopPolling();
  }

  getSnapshot() {
    return {
      enabled: Boolean(this.lastSnapshot?.enabled),
      intervalMs: Number(this.lastSnapshot?.intervalMs || DEFAULT_INTERVAL_MS),
      updatedAt: this.lastSnapshot?.updatedAt || null,
      sample: this.lastSnapshot?.sample || null,
      error: this.lastSnapshot?.error || null
    };
  }

  async handleSettingsChanged(changedKeys = []) {
    const normalizedKeys = (Array.isArray(changedKeys) ? changedKeys : [])
      .map((key) => String(key || '').trim().toLowerCase())
      .filter(Boolean);

    if (normalizedKeys.length === 0) {
      return;
    }

    const relevant = normalizedKeys.some((key) => RELEVANT_SETTINGS_KEYS.has(key));
    if (!relevant) {
      return;
    }

    await this.reloadFromSettings({
      forceImmediatePoll: true
    });
  }

  async reloadFromSettings(options = {}) {
    const forceBroadcast = Boolean(options?.forceBroadcast);
    const forceImmediatePoll = Boolean(options?.forceImmediatePoll);
    let settingsMap = {};
    try {
      settingsMap = await settingsService.getSettingsMap();
    } catch (error) {
      logger.warn('settings:load:failed', { error: errorToMeta(error) });
      return this.getSnapshot();
    }

    const nextEnabled = toBoolean(settingsMap.hardware_monitoring_enabled);
    const nextIntervalMs = clampIntervalMs(settingsMap.hardware_monitoring_interval_ms);
    const nextPaths = this.buildMonitoredPaths(settingsMap);
    const wasEnabled = this.enabled;
    const intervalChanged = nextIntervalMs !== this.intervalMs;
    const pathsChanged = this.pathsSignature(this.monitoredPaths) !== this.pathsSignature(nextPaths);

    this.enabled = nextEnabled;
    this.intervalMs = nextIntervalMs;
    this.monitoredPaths = nextPaths;
    this.lastSnapshot = {
      ...this.lastSnapshot,
      enabled: this.enabled,
      intervalMs: this.intervalMs
    };

    if (!this.enabled) {
      this.stopPolling();
      this.lastSnapshot = {
        enabled: false,
        intervalMs: this.intervalMs,
        updatedAt: nowIso(),
        sample: null,
        error: null
      };
      this.broadcastUpdate();
      return this.getSnapshot();
    }

    if (!this.running) {
      this.startPolling();
    } else if (intervalChanged || pathsChanged || forceImmediatePoll || !wasEnabled) {
      this.scheduleNext(25);
    }

    if (forceBroadcast || intervalChanged || !wasEnabled) {
      this.broadcastUpdate();
    }

    return this.getSnapshot();
  }

  buildMonitoredPaths(settingsMap = {}) {
    const sourceMap = settingsMap && typeof settingsMap === 'object' ? settingsMap : {};
    const bluray = settingsService.resolveEffectiveToolSettings(sourceMap, 'bluray');
    const dvd = settingsService.resolveEffectiveToolSettings(sourceMap, 'dvd');
    const cd = settingsService.resolveEffectiveToolSettings(sourceMap, 'cd');
    const blurayRawPath = normalizePathSetting(bluray?.raw_dir);
    const dvdRawPath = normalizePathSetting(dvd?.raw_dir);
    const cdRawPath = normalizePathSetting(cd?.raw_dir);
    const blurayMoviePath = normalizePathSetting(bluray?.movie_dir);
    const dvdMoviePath = normalizePathSetting(dvd?.movie_dir);
    const externalStoragePaths = parseExternalStoragePaths(sourceMap.external_storage_paths);
    const monitoredPaths = [];

    const addPath = (key, label, monitoredPath, options = {}) => {
      monitoredPaths.push({
        key,
        label,
        path: normalizePathSetting(monitoredPath),
        hideWhenUnavailable: Boolean(options?.hideWhenUnavailable),
        requireDedicatedMount: Boolean(options?.requireDedicatedMount)
      });
    };

    if (blurayRawPath && dvdRawPath && blurayRawPath !== dvdRawPath) {
      addPath('raw_dir_bluray', 'RAW-Verzeichnis (Blu-ray)', blurayRawPath);
      addPath('raw_dir_dvd', 'RAW-Verzeichnis (DVD)', dvdRawPath);
    } else {
      addPath('raw_dir', 'RAW-Verzeichnis', blurayRawPath || dvdRawPath || sourceMap.raw_dir);
    }
    addPath('raw_dir_cd', 'CD RAW-Ordner', cdRawPath || sourceMap.raw_dir_cd);

    if (blurayMoviePath && dvdMoviePath && blurayMoviePath !== dvdMoviePath) {
      addPath('movie_dir_bluray', 'Movie-Verzeichnis (Blu-ray)', blurayMoviePath);
      addPath('movie_dir_dvd', 'Movie-Verzeichnis (DVD)', dvdMoviePath);
    } else {
      addPath('movie_dir', 'Movie-Verzeichnis', blurayMoviePath || dvdMoviePath || sourceMap.movie_dir);
    }

    addPath('log_dir', 'Log-Verzeichnis', sourceMap.log_dir);
    externalStoragePaths.forEach((externalEntry, index) => {
      const externalPath = normalizePathSetting(externalEntry?.path);
      const externalLabel = normalizePathSetting(externalEntry?.label) || `Externer Speicher ${index + 1}`;
      addPath(
        `external_storage_path_${index}`,
        externalLabel,
        externalPath,
        {
          hideWhenUnavailable: true,
          requireDedicatedMount: true
        }
      );
    });

    return monitoredPaths;
  }

  pathsSignature(paths = []) {
    return (Array.isArray(paths) ? paths : [])
      .map((item) => `${String(item?.key || '')}:${String(item?.path || '')}`)
      .join('|');
  }

  startPolling() {
    if (this.running) {
      return;
    }
    this.running = true;
    logger.info('start', {
      intervalMs: this.intervalMs,
      pathKeys: this.monitoredPaths.map((item) => item.key)
    });
    this.scheduleNext(20);
  }

  stopPolling() {
    const wasActive = this.running || this.pollInFlight || Boolean(this.timer);
    this.running = false;
    this.pollInFlight = false;
    this.lastCpuTimes = null;
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    if (wasActive) {
      logger.info('stop');
    }
  }

  scheduleNext(delayMs) {
    if (!this.running) {
      return;
    }
    if (this.timer) {
      clearTimeout(this.timer);
    }
    const delay = Math.max(0, Math.trunc(Number(delayMs) || this.intervalMs));
    this.timer = setTimeout(() => {
      this.timer = null;
      void this.pollOnce();
    }, delay);
  }

  async pollOnce() {
    if (!this.running || !this.enabled) {
      return;
    }
    if (this.pollInFlight) {
      this.scheduleNext(this.intervalMs);
      return;
    }
    this.pollInFlight = true;
    try {
      const sample = await this.collectSample();
      this.lastSnapshot = {
        enabled: true,
        intervalMs: this.intervalMs,
        updatedAt: nowIso(),
        sample,
        error: null
      };
      this.broadcastUpdate();
    } catch (error) {
      logger.warn('poll:failed', { error: errorToMeta(error) });
      this.lastSnapshot = {
        ...this.lastSnapshot,
        enabled: true,
        intervalMs: this.intervalMs,
        updatedAt: nowIso(),
        error: error?.message || 'Hardware-Monitoring fehlgeschlagen.'
      };
      this.broadcastUpdate();
    } finally {
      this.pollInFlight = false;
      if (this.running && this.enabled) {
        this.scheduleNext(this.intervalMs);
      }
    }
  }

  broadcastUpdate() {
    wsService.broadcast('HARDWARE_MONITOR_UPDATE', this.getSnapshot());
  }

  async collectSample() {
    const memory = this.collectMemoryMetrics();
    const [cpu, gpu, storage] = await Promise.all([
      this.collectCpuMetrics(),
      this.collectGpuMetrics(),
      this.collectStorageMetrics()
    ]);

    return {
      cpu,
      memory,
      gpu,
      storage
    };
  }

  collectMemoryMetrics() {
    const totalBytes = Number(os.totalmem() || 0);
    const freeBytes = Number(os.freemem() || 0);
    const usedBytes = Math.max(0, totalBytes - freeBytes);
    const usagePercent = totalBytes > 0
      ? roundNumber((usedBytes / totalBytes) * 100, 1)
      : null;
    return {
      totalBytes,
      usedBytes,
      freeBytes,
      usagePercent
    };
  }

  getCpuTimes() {
    const cpus = os.cpus() || [];
    return cpus.map((cpu) => {
      const times = cpu?.times || {};
      const idle = Number(times.idle || 0);
      const total = Object.values(times).reduce((sum, value) => sum + Number(value || 0), 0);
      return { idle, total };
    });
  }

  calculateCpuUsage(currentTimes = [], previousTimes = []) {
    const perCore = [];
    const coreCount = Math.min(currentTimes.length, previousTimes.length);
    if (coreCount <= 0) {
      return {
        overallUsagePercent: null,
        perCore
      };
    }

    let totalDelta = 0;
    let idleDelta = 0;
    for (let index = 0; index < coreCount; index += 1) {
      const prev = previousTimes[index];
      const cur = currentTimes[index];
      const deltaTotal = Number(cur?.total || 0) - Number(prev?.total || 0);
      const deltaIdle = Number(cur?.idle || 0) - Number(prev?.idle || 0);
      const usage = deltaTotal > 0
        ? roundNumber(((deltaTotal - deltaIdle) / deltaTotal) * 100, 1)
        : null;
      perCore.push({
        index,
        usagePercent: usage
      });
      if (deltaTotal > 0) {
        totalDelta += deltaTotal;
        idleDelta += deltaIdle;
      }
    }

    const overallUsagePercent = totalDelta > 0
      ? roundNumber(((totalDelta - idleDelta) / totalDelta) * 100, 1)
      : null;
    return {
      overallUsagePercent,
      perCore
    };
  }

  async collectCpuMetrics() {
    const cpus = os.cpus() || [];
    const currentTimes = this.getCpuTimes();
    const usage = this.calculateCpuUsage(currentTimes, this.lastCpuTimes || []);
    this.lastCpuTimes = currentTimes;

    const tempMetrics = await this.collectCpuTemperatures();
    const tempByCoreIndex = new Map(
      (tempMetrics.perCore || []).map((item) => [Number(item.index), item.temperatureC])
    );

    const perCore = usage.perCore.map((entry) => ({
      index: entry.index,
      usagePercent: entry.usagePercent,
      temperatureC: tempByCoreIndex.has(entry.index) ? tempByCoreIndex.get(entry.index) : null
    }));

    for (const tempEntry of tempMetrics.perCore || []) {
      const index = Number(tempEntry?.index);
      if (!Number.isFinite(index) || perCore.some((item) => item.index === index)) {
        continue;
      }
      perCore.push({
        index,
        usagePercent: null,
        temperatureC: tempEntry.temperatureC
      });
    }
    perCore.sort((a, b) => a.index - b.index);

    return {
      model: cpus[0]?.model || null,
      logicalCoreCount: cpus.length,
      loadAverage: os.loadavg().map((value) => roundNumber(value, 2)),
      overallUsagePercent: usage.overallUsagePercent,
      overallTemperatureC: tempMetrics.overallC,
      usageAvailable: usage.overallUsagePercent !== null,
      temperatureAvailable: Boolean(tempMetrics.available),
      temperatureSource: tempMetrics.source,
      perCore
    };
  }

  async collectCpuTemperatures() {
    const sensors = await this.collectTempsViaSensors();
    if (sensors.available) {
      return sensors;
    }

    const hwmon = this.collectTempsViaHwmon();
    if (hwmon.available) {
      return hwmon;
    }

    const thermalZones = this.collectTempsViaThermalZones();
    if (thermalZones.available) {
      return thermalZones;
    }

    return {
      source: 'none',
      overallC: null,
      perCore: [],
      available: false
    };
  }

  async collectTempsViaSensors() {
    if (this.sensorsCommandAvailable === false) {
      return {
        source: 'sensors',
        overallC: null,
        perCore: [],
        available: false
      };
    }

    try {
      const { stdout } = await execFileAsync('sensors', ['-j'], {
        timeout: SENSORS_TIMEOUT_MS,
        maxBuffer: 2 * 1024 * 1024
      });
      this.sensorsCommandAvailable = true;
      const parsed = JSON.parse(String(stdout || '{}'));
      const candidates = collectTemperatureCandidates(parsed);
      const preferred = preferCpuTemperatureCandidates(candidates);
      return {
        source: 'sensors',
        ...mapTemperatureCandidates(preferred)
      };
    } catch (error) {
      if (isCommandMissingError(error)) {
        this.sensorsCommandAvailable = false;
      }
      logger.debug('cpu-temp:sensors:failed', { error: errorToMeta(error) });
      return {
        source: 'sensors',
        overallC: null,
        perCore: [],
        available: false
      };
    }
  }

  collectTempsViaHwmon() {
    const hwmonRoot = '/sys/class/hwmon';
    if (!fs.existsSync(hwmonRoot)) {
      return {
        source: 'hwmon',
        overallC: null,
        perCore: [],
        available: false
      };
    }

    const candidates = [];
    let dirs = [];
    try {
      dirs = fs.readdirSync(hwmonRoot, { withFileTypes: true });
    } catch (_error) {
      dirs = [];
    }

    for (const dir of dirs) {
      if (!dir.isDirectory()) {
        continue;
      }
      const basePath = path.join(hwmonRoot, dir.name);
      const sensorName = readTextFileSafe(path.join(basePath, 'name')) || dir.name;
      let files = [];
      try {
        files = fs.readdirSync(basePath);
      } catch (_error) {
        files = [];
      }
      const tempInputFiles = files.filter((file) => /^temp\d+_input$/i.test(file));

      for (const fileName of tempInputFiles) {
        const tempValue = normalizeTempC(readTextFileSafe(path.join(basePath, fileName)));
        if (tempValue === null) {
          continue;
        }
        const labelFile = fileName.replace('_input', '_label');
        const label = readTextFileSafe(path.join(basePath, labelFile)) || fileName;
        candidates.push({
          label: `${sensorName} / ${label}`,
          value: tempValue
        });
      }
    }

    return {
      source: 'hwmon',
      ...mapTemperatureCandidates(preferCpuTemperatureCandidates(candidates))
    };
  }

  collectTempsViaThermalZones() {
    const thermalRoot = '/sys/class/thermal';
    if (!fs.existsSync(thermalRoot)) {
      return {
        source: 'thermal_zone',
        overallC: null,
        perCore: [],
        available: false
      };
    }

    let files = [];
    try {
      files = fs.readdirSync(thermalRoot, { withFileTypes: true });
    } catch (_error) {
      files = [];
    }

    const candidates = [];
    for (const dir of files) {
      if (!dir.isDirectory() || !dir.name.startsWith('thermal_zone')) {
        continue;
      }
      const basePath = path.join(thermalRoot, dir.name);
      const tempC = normalizeTempC(readTextFileSafe(path.join(basePath, 'temp')));
      if (tempC === null) {
        continue;
      }
      const zoneType = readTextFileSafe(path.join(basePath, 'type')) || dir.name;
      candidates.push({
        label: `${zoneType} / temp`,
        value: tempC
      });
    }

    return {
      source: 'thermal_zone',
      ...mapTemperatureCandidates(preferCpuTemperatureCandidates(candidates))
    };
  }

  async collectGpuMetrics() {
    if (this.nvidiaSmiAvailable === false) {
      return {
        source: 'nvidia-smi',
        available: false,
        devices: [],
        message: 'nvidia-smi ist nicht verfuegbar.'
      };
    }

    try {
      const { stdout } = await execFileAsync(
        'nvidia-smi',
        [
          '--query-gpu=index,name,utilization.gpu,utilization.memory,temperature.gpu,memory.used,memory.total,power.draw,power.limit,fan.speed',
          '--format=csv,noheader,nounits'
        ],
        {
          timeout: NVIDIA_SMI_TIMEOUT_MS,
          maxBuffer: 1024 * 1024
        }
      );

      this.nvidiaSmiAvailable = true;
      const devices = String(stdout || '')
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean)
        .map((line) => parseNvidiaCsvLine(line))
        .filter(Boolean);

      if (devices.length === 0) {
        return {
          source: 'nvidia-smi',
          available: false,
          devices: [],
          message: 'Keine GPU-Daten ueber nvidia-smi erkannt.'
        };
      }

      return {
        source: 'nvidia-smi',
        available: true,
        devices,
        message: null
      };
    } catch (error) {
      const commandMissing = isCommandMissingError(error);
      if (commandMissing) {
        this.nvidiaSmiAvailable = false;
      }
      logger.debug('gpu:nvidia-smi:failed', { error: errorToMeta(error) });
      return {
        source: 'nvidia-smi',
        available: false,
        devices: [],
        message: commandMissing
          ? 'nvidia-smi ist nicht verfuegbar.'
          : (String(error?.stderr || error?.message || 'GPU-Abfrage fehlgeschlagen').trim().slice(0, 220))
      };
    }
  }

  async collectStorageMetrics() {
    const list = [];
    for (const entry of this.monitoredPaths) {
      const metric = await this.collectStorageForPath(entry);
      if (metric && !metric.hidden) {
        list.push(metric);
      }
    }
    return list;
  }

  findNearestExistingPath(inputPath) {
    const normalized = String(inputPath || '').trim();
    if (!normalized) {
      return null;
    }
    let candidate = path.resolve(normalized);
    for (let depth = 0; depth < 64; depth += 1) {
      if (fs.existsSync(candidate)) {
        return candidate;
      }
      const parent = path.dirname(candidate);
      if (!parent || parent === candidate) {
        break;
      }
      candidate = parent;
    }
    if (fs.existsSync(candidate)) {
      return candidate;
    }
    return null;
  }

  async collectStorageForPath(entry) {
    const key = String(entry?.key || '');
    const label = String(entry?.label || key || 'Pfad');
    const rawPath = String(entry?.path || '').trim();
    const hideWhenUnavailable = Boolean(entry?.hideWhenUnavailable);
    const requireDedicatedMount = Boolean(entry?.requireDedicatedMount);

    const hiddenResult = (base = {}) => ({
      key,
      label,
      hidden: true,
      ...base
    });

    if (!rawPath) {
      if (hideWhenUnavailable) {
        return hiddenResult({
          path: null,
          queryPath: null,
          exists: false
        });
      }
      return {
        key,
        label,
        path: null,
        queryPath: null,
        exists: false,
        totalBytes: null,
        usedBytes: null,
        freeBytes: null,
        usagePercent: null,
        mountPoint: null,
        note: null,
        error: 'Pfad ist leer.'
      };
    }

    const resolvedPath = path.isAbsolute(rawPath) ? path.normalize(rawPath) : path.resolve(rawPath);
    const exists = fs.existsSync(resolvedPath);
    const queryPath = exists ? resolvedPath : this.findNearestExistingPath(resolvedPath);

    if (!queryPath) {
      if (hideWhenUnavailable) {
        return hiddenResult({
          path: resolvedPath,
          queryPath: null,
          exists: false
        });
      }
      return {
        key,
        label,
        path: resolvedPath,
        queryPath: null,
        exists: false,
        totalBytes: null,
        usedBytes: null,
        freeBytes: null,
        usagePercent: null,
        mountPoint: null,
        note: null,
        error: 'Pfad oder Parent existiert nicht.'
      };
    }

    try {
      const { stdout } = await execFileAsync('df', ['-Pk', queryPath], {
        timeout: DF_TIMEOUT_MS,
        maxBuffer: 256 * 1024
      });
      const parsed = parseDfStats(stdout);
      if (!parsed) {
        if (hideWhenUnavailable) {
          return hiddenResult({
            path: resolvedPath,
            queryPath,
            exists
          });
        }
        return {
          key,
          label,
          path: resolvedPath,
          queryPath,
          exists,
          totalBytes: null,
          usedBytes: null,
          freeBytes: null,
          usagePercent: null,
          mountPoint: null,
          note: exists ? null : `Pfad fehlt, Parent verwendet (${queryPath}).`,
          error: 'Dateisystemdaten konnten nicht geparst werden.'
        };
      }
      const mountPoint = parsed?.mountPoint ? path.resolve(parsed.mountPoint) : parsed.mountPoint;
      const hasDedicatedMount = Boolean(mountPoint) && !isRootPath(mountPoint);
      const shouldShowExternal = exists || hasDedicatedMount;
      if (hideWhenUnavailable && requireDedicatedMount && !shouldShowExternal) {
        return hiddenResult({
          path: resolvedPath,
          queryPath,
          exists
        });
      }

      return {
        key,
        label,
        path: resolvedPath,
        queryPath,
        exists,
        ...parsed,
        mountPoint: mountPoint || null,
        note: exists ? null : `Pfad fehlt, Parent verwendet (${queryPath}).`,
        error: null
      };
    } catch (error) {
      if (hideWhenUnavailable) {
        return hiddenResult({
          path: resolvedPath,
          queryPath,
          exists
        });
      }
      return {
        key,
        label,
        path: resolvedPath,
        queryPath,
        exists,
        totalBytes: null,
        usedBytes: null,
        freeBytes: null,
        usagePercent: null,
        mountPoint: null,
        note: null,
        error: String(error?.message || 'df Abfrage fehlgeschlagen')
      };
    }
  }
}

module.exports = new HardwareMonitorService();
