import { runSSH } from '../lib/ssh.js';
import { vmManager } from './VMManager.js';

function shellQuote(value) {
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

function normalizeMode(value) {
  return String(value ?? '').replace(/^0+/, '').padStart(4, '0');
}

function buildFailure(rule, detail = '') {
  const vm = rule.vm ? `${rule.vm}:` : '';
  const path = rule.path ?? rule.service ?? rule.package ?? rule.command ?? rule.port ?? '';
  const suffix = detail ? ` (${detail})` : '';
  return `${vm}${rule.type}:${path}${suffix}`;
}

export class ValidationEngine {
  constructor({ vmManager: vmManagerInstance = vmManager, sshRunner = runSSH } = {}) {
    this.vmManager = vmManagerInstance;
    this.sshRunner = sshRunner;
  }

  async resolveBranch(quest) {
    const branches = [...(quest.solution_branches ?? [])].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
    const failures = [];

    for (const branch of branches) {
      const result = await this.evaluateBranch(branch);
      if (result.passed) {
        return {
          branch,
          failures: []
        };
      }

      failures.push(...result.failures.map((failure) => `${branch.id}:${failure}`));
    }

    return {
      branch: null,
      failures
    };
  }

  async evaluateBranch(branch) {
    return await this.evaluateRule(branch.validation ?? {}, '');
  }

  async evaluateRule(rule, defaultVmId = '') {
    const type = rule?.type ?? '';
    const targetVm = rule?.vm ?? defaultVmId;

    switch (type) {
      case 'and':
        return await this._evalAnd(rule, targetVm);
      case 'or':
        return await this._evalOr(rule, targetVm);
      case 'not':
        return await this._evalNot(rule, targetVm);
      case 'file_exists':
        return await this._remoteAssert(targetVm, `test -e ${shellQuote(rule.path)}`, rule);
      case 'file_absent':
        return await this._remoteAssert(targetVm, `test ! -e ${shellQuote(rule.path)}`, rule);
      case 'directory_exists':
        return await this._remoteAssert(targetVm, `test -d ${shellQuote(rule.path)}`, rule);
      case 'file_contains':
      case 'log_contains':
        return await this._remoteAssert(
          targetVm,
          `grep -qF ${shellQuote(rule.contains ?? '')} ${shellQuote(rule.path)}`,
          rule
        );
      case 'file_mode':
      case 'file_mode_matches':
        return await this._checkFileMode(targetVm, rule);
      case 'file_owner':
      case 'file_owner_matches':
        return await this._checkFileOwner(targetVm, rule, false);
      case 'file_owner_is_not':
        return await this._checkFileOwner(targetVm, {
          ...rule,
          user: rule.expected_user ?? rule.user ?? '',
          group: rule.expected_group ?? rule.group ?? ''
        }, true);
      case 'service_state':
      case 'service_state_is':
      case 'service_state_matches':
        return await this._checkServiceState(targetVm, rule);
      case 'service_enabled':
      case 'service_enabled_is':
        return await this._checkServiceEnabled(targetVm, rule);
      case 'process_running':
        return await this._remoteAssert(targetVm, `pgrep -x ${shellQuote(rule.process)}`, rule);
      case 'process_user':
        return await this._remoteAssert(targetVm, `pgrep -x -u ${shellQuote(rule.user)} ${shellQuote(rule.process)}`, rule);
      case 'port_listening':
        return await this._checkPortListening(targetVm, rule);
      case 'package_installed':
        return await this._checkPackageInstalled(targetVm, rule);
      case 'mount_present':
        return await this._remoteAssert(targetVm, `findmnt -M ${shellQuote(rule.path)}`, rule);
      case 'disk_usage_below':
        return await this._checkDiskUsage(targetVm, rule, 'below');
      case 'disk_usage_above':
        return await this._checkDiskUsage(targetVm, rule, 'above');
      case 'command_assert':
        return await this._checkCommandAssert(targetVm, rule);
      default:
        return {
          passed: false,
          failures: [buildFailure(rule, 'unsupported-rule')]
        };
    }
  }

  async _evalAnd(rule, targetVm) {
    const failures = [];
    for (const subRule of rule.rules ?? []) {
      const result = await this.evaluateRule(subRule, targetVm);
      if (!result.passed) {
        failures.push(...result.failures);
      }
    }

    return {
      passed: failures.length === 0,
      failures
    };
  }

  async _evalOr(rule, targetVm) {
    const failures = [];
    for (const subRule of rule.rules ?? []) {
      const result = await this.evaluateRule(subRule, targetVm);
      if (result.passed) {
        return { passed: true, failures: [] };
      }

      failures.push(...result.failures);
    }

    return {
      passed: false,
      failures: failures.length > 0 ? failures : [buildFailure(rule, 'no-branches-matched')]
    };
  }

  async _evalNot(rule, targetVm) {
    const result = await this.evaluateRule(rule.rule ?? {}, targetVm);
    return {
      passed: !result.passed,
      failures: result.passed ? [buildFailure(rule.rule ?? rule, 'negated-rule-matched')] : []
    };
  }

  async _checkFileMode(targetVm, rule) {
    const result = await this._remoteExec(
      targetVm,
      `stat -c %a ${shellQuote(rule.path)}`
    );

    if (!result.ok) {
      return { passed: false, failures: [buildFailure(rule)] };
    }

    const actual = normalizeMode(result.stdout.trim());
    const expected = normalizeMode(rule.mode);
    return {
      passed: actual === expected,
      failures: actual === expected ? [] : [buildFailure(rule, `expected ${expected}, got ${actual}`)]
    };
  }

  async _checkFileOwner(targetVm, rule, negate = false) {
    const result = await this._remoteExec(
      targetVm,
      `stat -c %U:%G ${shellQuote(rule.path)}`
    );

    if (!result.ok) {
      return { passed: false, failures: [buildFailure(rule)] };
    }

    const [actualUser = '', actualGroup = ''] = result.stdout.trim().split(':');
    const expectedUser = rule.user ?? '';
    const expectedGroup = rule.group ?? '';
    const matches = (!expectedUser || actualUser === expectedUser) && (!expectedGroup || actualGroup === expectedGroup);
    const passed = negate ? !matches : matches;

    return {
      passed,
      failures: passed ? [] : [buildFailure(rule, `got ${actualUser}:${actualGroup}`)]
    };
  }

  async _checkServiceState(targetVm, rule) {
    const result = await this._remoteExec(
      targetVm,
      `systemctl is-active ${shellQuote(rule.service)}`
    );

    const actual = result.ok ? result.stdout.trim() : result.stdout.trim() || result.stderr.trim() || 'unknown';
    const expected = rule.state ?? 'active';
    return {
      passed: actual === expected,
      failures: actual === expected ? [] : [buildFailure(rule, `expected ${expected}, got ${actual}`)]
    };
  }

  async _checkServiceEnabled(targetVm, rule) {
    const result = await this._remoteExec(
      targetVm,
      `systemctl is-enabled ${shellQuote(rule.service)}`
    );

    const expected = rule.enabled ?? true;
    const actualEnabled = result.ok && result.stdout.trim() === 'enabled';
    return {
      passed: actualEnabled === expected,
      failures: actualEnabled === expected ? [] : [buildFailure(rule, `expected enabled=${expected}`)]
    };
  }

  async _checkPortListening(targetVm, rule) {
    const port = Number(rule.port);
    const protocol = String(rule.protocol ?? 'tcp').toLowerCase();
    const listening = rule.listening ?? true;
    const ssFlags = protocol === 'udp' ? '-lun' : '-ltn';
    const result = await this._remoteExec(
      targetVm,
      `ss ${ssFlags} | grep -Eq '[:.]${port}(\\s|$)'`
    );

    return {
      passed: result.ok === listening,
      failures: result.ok === listening ? [] : [buildFailure(rule, `expected listening=${listening}`)]
    };
  }

  async _checkPackageInstalled(targetVm, rule) {
    const packageSpec = String(rule.package ?? '');
    const shouldBeInstalled = rule.installed ?? true;
    const distro = this.vmManager.getDistro(targetVm);
    const [packageName, version] = packageSpec.split('=');

    let command = '';
    if (distro === 'arch') {
      command = version
        ? `pacman -Q ${shellQuote(packageName)} | grep -F ${shellQuote(version)}`
        : `pacman -Q ${shellQuote(packageName)}`;
    } else {
      command = version
        ? `dpkg-query -W -f='\\${Status} \\${Version}\\n' ${shellQuote(packageName)} | grep -F ${shellQuote(`install ok installed ${version}`)}`
        : `dpkg-query -W -f='\\${Status}\\n' ${shellQuote(packageName)} | grep -F 'install ok installed'`;
    }

    const result = await this._remoteExec(targetVm, command);
    return {
      passed: result.ok === shouldBeInstalled,
      failures: result.ok === shouldBeInstalled ? [] : [buildFailure(rule, `expected installed=${shouldBeInstalled}`)]
    };
  }

  async _checkDiskUsage(targetVm, rule, mode) {
    const path = rule.path ?? '/';
    const threshold = Number(rule.threshold_percent ?? rule.percent ?? 0);
    const result = await this._remoteExec(
      targetVm,
      `df -P ${shellQuote(path)} | tail -1 | awk '{print $5}' | tr -d '% '`
    );

    if (!result.ok) {
      return { passed: false, failures: [buildFailure(rule)] };
    }

    const actual = Number(result.stdout.trim());
    const passed = mode === 'below' ? actual < threshold : actual > threshold;
    return {
      passed,
      failures: passed ? [] : [buildFailure(rule, `expected ${mode} ${threshold}, got ${actual}`)]
    };
  }

  async _checkCommandAssert(targetVm, rule) {
    const result = await this._remoteExec(targetVm, rule.command ?? '', { rawShell: true });
    const expectedExitCode = Number(rule.exit_code ?? 0);
    const stdoutContains = rule.stdout_contains ?? rule.contains ?? '';
    const passedExit = result.code === expectedExitCode;
    const passedOutput = !stdoutContains || result.stdout.includes(stdoutContains);
    const passed = passedExit && passedOutput;

    return {
      passed,
      failures: passed ? [] : [buildFailure(rule, `expected exit=${expectedExitCode}`)]
    };
  }

  async _remoteAssert(targetVm, shellCommand, rule) {
    const result = await this._remoteExec(targetVm, shellCommand);
    return {
      passed: result.ok,
      failures: result.ok ? [] : [buildFailure(rule)]
    };
  }

  async _remoteExec(targetVm, shellCommand, options = {}) {
    const host = await this.vmManager.getIP(targetVm);
    if (!host) {
      return {
        ok: false,
        code: 255,
        stdout: '',
        stderr: `Unable to resolve host for ${targetVm}`,
        command: shellCommand
      };
    }

    const managementUser = this.vmManager.getManagementUser(targetVm);
    const keyPath = this.vmManager.getSshKeyPath(targetVm);
    const wrappedCommand = `sudo bash -lc ${shellQuote(shellCommand)}`;

    return await this.sshRunner({
      host,
      user: managementUser,
      keyPath,
      command: wrappedCommand,
      timeoutSec: options.timeoutSec ?? 15
    });
  }
}

export const validationEngine = new ValidationEngine();
