import { Buffer } from 'node:buffer';
import { Agent, fetch as undiciFetch } from 'undici';
import { config } from '../config.js';
import { getBooleanSetting, getStringSetting } from '../models/settingsModel.js';
import { normalizeOsFamily } from '../utils/osFamily.js';

const WINDOWS_HINTS = ['windows', 'microsoft'];
const LINUX_HINTS = ['linux', 'ubuntu', 'debian', 'red hat', 'rhel', 'centos', 'suse', 'oracle linux'];
const MAX_TRACE_ENTRIES = 700;
const MAX_TRACE_BODY_CHARS = 6000;
const VM_ENRICHMENT_CONCURRENCY = 8;
const WEB_SERVICES_PROFILE_LABEL = 'vSphere Web Services API';
const API_PROFILES = {
  api: {
    mode: 'api',
    label: 'vSphere Automation API',
    sessionPath: '/api/session',
    vmListPath: '/api/vcenter/vm',
    vmNameParam: 'names',
    vmDetailPath: (vmId) => `/api/vcenter/vm/${encodeURIComponent(vmId)}`,
    guestIdentityPath: (vmId) => `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`,
    guestNetworkingPath: (vmId) => `/api/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`
  },
  rest: {
    mode: 'rest',
    label: 'Legacy vCenter REST API',
    sessionPath: '/rest/com/vmware/cis/session',
    vmListPath: '/rest/vcenter/vm',
    vmNameParam: 'filter.names',
    vmDetailPath: (vmId) => `/rest/vcenter/vm/${encodeURIComponent(vmId)}`,
    guestIdentityPath: (vmId) => `/rest/vcenter/vm/${encodeURIComponent(vmId)}/guest/identity`,
    guestNetworkingPath: (vmId) => `/rest/vcenter/vm/${encodeURIComponent(vmId)}/guest/networking/interfaces`
  }
};

export class VCenterTraceError extends Error {
  constructor(message, trace = [], statusCode = 502) {
    super(message);
    this.name = 'VCenterTraceError';
    this.trace = trace;
    this.statusCode = statusCode;
  }
}

export function getVCenterSettings() {
  const apiMode = getStringSetting('vcenter_api_mode', config.vcenter.apiMode || 'auto').toLowerCase();
  const requestTimeoutMs = Number(getStringSetting('vcenter_request_timeout_ms', String(config.vcenter.requestTimeoutMs || 20000)));
  return {
    enabled: getBooleanSetting('vcenter_enabled', config.vcenter.enabled),
    targetType: 'vcenter',
    baseUrl: normalizeBaseUrl(getStringSetting('vcenter_base_url', config.vcenter.baseUrl)),
    username: getStringSetting('vcenter_username', config.vcenter.username),
    password: getStringSetting('vcenter_password', config.vcenter.password),
    apiMode: ['auto', 'api', 'rest'].includes(apiMode) ? apiMode : 'auto',
    requestTimeoutMs: Number.isFinite(requestTimeoutMs) && requestTimeoutMs > 0 ? requestTimeoutMs : 20000,
    allowUntrustedTls: getBooleanSetting('vcenter_allow_untrusted_tls', config.vcenter.allowUntrustedTls)
  };
}

export function settingsFromVCenterConnection(connection) {
  if (!connection) return getVCenterSettings();
  const apiMode = String(connection.api_mode || connection.apiMode || 'auto').toLowerCase();
  const targetType = String(connection.target_type || connection.targetType || 'vcenter').toLowerCase() === 'host' ? 'host' : 'vcenter';
  return {
    enabled: Boolean(connection.enabled ?? true),
    targetType,
    baseUrl: normalizeBaseUrl(connection.base_url || connection.baseUrl),
    username: connection.username || '',
    password: connection.password || '',
    apiMode: targetType === 'host'
      ? 'web-services'
      : (['auto', 'api', 'rest'].includes(apiMode) ? apiMode : 'auto'),
    requestTimeoutMs: Number(connection.request_timeout_ms || connection.requestTimeoutMs || 20000),
    allowUntrustedTls: Boolean(connection.allow_untrusted_tls ?? connection.allowUntrustedTls)
  };
}

export function normalizeBaseUrl(value) {
  return String(value || '').trim().replace(/\/+$/, '');
}

export function vcenterStatus() {
  const settings = getVCenterSettings();
  return {
    enabled: settings.enabled,
    configured: Boolean(settings.baseUrl && settings.username && settings.password),
    targetType: settings.targetType,
    baseUrl: settings.baseUrl,
    username: settings.username,
    apiMode: settings.apiMode,
    requestTimeoutMs: settings.requestTimeoutMs,
    allowUntrustedTls: settings.allowUntrustedTls
  };
}

export function vcenterConnectionStatus(connection) {
  const settings = settingsFromVCenterConnection(connection);
  return {
    id: connection?.id || '',
    name: connection?.name || 'Configured default',
    enabled: settings.enabled,
    configured: Boolean(settings.baseUrl && settings.username && settings.password),
    targetType: settings.targetType,
    baseUrl: settings.baseUrl,
    username: settings.username,
    apiMode: settings.apiMode,
    requestTimeoutMs: settings.requestTimeoutMs,
    allowUntrustedTls: settings.allowUntrustedTls
  };
}

function assertConfigured(settings) {
  const label = settings.targetType === 'host' ? 'VMware standalone host' : 'VMware vCenter';
  if (!settings.enabled) throw new Error(`${label} integration is disabled in Configuration.`);
  if (!settings.baseUrl) throw new Error(`${label} base URL is missing.`);
  if (!settings.username || !settings.password) throw new Error(`${label} credentials are missing.`);
}

function dispatcherFor(settings) {
  return settings.allowUntrustedTls
    ? new Agent({ connect: { rejectUnauthorized: false } })
    : undefined;
}

function fetchForDispatcher(dispatcher) {
  // Node's built-in fetch and the npm undici Agent can be different major
  // versions. Use package fetch whenever we pass a package dispatcher.
  return dispatcher ? undiciFetch : fetch;
}

async function parseVCenterResponse(response) {
  const text = await response.text();
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch {
    return text.replace(/^"|"$/g, '');
  }
}

function summarizePayload(payload) {
  if (Array.isArray(payload)) return { type: 'array', count: payload.length };
  if (Array.isArray(payload?.value)) return { type: 'value-array', count: payload.value.length };
  if (payload && typeof payload === 'object') return { type: 'object', keys: Object.keys(payload).slice(0, 20) };
  return { type: typeof payload };
}

function bodyPreview(payload, path) {
  if (path === '/api/session' || path === '/rest/com/vmware/cis/session') return '[redacted vCenter session id]';
  const raw = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
  const text = raw || '';
  return text.length > MAX_TRACE_BODY_CHARS
    ? `${text.slice(0, MAX_TRACE_BODY_CHARS)}\n... truncated ${text.length - MAX_TRACE_BODY_CHARS} character(s)`
    : text;
}

function pushTrace(trace, entry) {
  if (!trace) return;
  if (trace.length < MAX_TRACE_ENTRIES) {
    trace.push(entry);
    return;
  }
  if (!trace.some((item) => item.truncated)) {
    trace.push({
      truncated: true,
      message: `vCenter trace limited to ${MAX_TRACE_ENTRIES} entries. Narrow the filter to capture fewer guest enrichment calls.`
    });
  }
}

function pushTraceInfo(trace, message, details = {}) {
  pushTrace(trace, {
    info: true,
    message,
    details
  });
}

async function vcenterFetch(settings, path, { method = 'GET', sessionId, allow404 = false, trace = null } = {}) {
  const headers = { Accept: 'application/json' };
  if (sessionId) headers['vmware-api-session-id'] = sessionId;
  else headers.Authorization = `Basic ${Buffer.from(`${settings.username}:${settings.password}`).toString('base64')}`;

  const startedAt = Date.now();
  let response;
  let payload = null;
  try {
    const dispatcher = dispatcherFor(settings);
    response = await fetchForDispatcher(dispatcher)(`${settings.baseUrl}${path}`, {
      method,
      headers,
      dispatcher,
      signal: AbortSignal.timeout(settings.requestTimeoutMs)
    });
    payload = await parseVCenterResponse(response);
  } catch (err) {
    pushTrace(trace, {
      method,
      url: `${settings.baseUrl}${path}`,
      requestHeaders: {
        Accept: headers.Accept,
        Authorization: headers.Authorization ? '[redacted basic auth]' : undefined,
        'vmware-api-session-id': headers['vmware-api-session-id'] ? '[redacted session id]' : undefined
      },
      durationMs: Date.now() - startedAt,
      timeoutMs: settings.requestTimeoutMs,
      networkError: err.name === 'TimeoutError'
        ? `Timed out after ${settings.requestTimeoutMs} ms waiting for vCenter.`
        : err.message
    });
    const message = err.name === 'TimeoutError'
      ? `Timed out after ${settings.requestTimeoutMs} ms waiting for vCenter ${method} ${path}.`
      : `Unable to reach vCenter ${method} ${path}: ${err.message}`;
    throw new VCenterTraceError(message, trace);
  }

  pushTrace(trace, {
    method,
    url: `${settings.baseUrl}${path}`,
    requestHeaders: {
      Accept: headers.Accept,
      Authorization: headers.Authorization ? '[redacted basic auth]' : undefined,
      'vmware-api-session-id': headers['vmware-api-session-id'] ? '[redacted session id]' : undefined
    },
    status: response.status,
    statusText: response.statusText,
    durationMs: Date.now() - startedAt,
    responseHeaders: {
      'content-type': response.headers.get('content-type') || '',
      date: response.headers.get('date') || ''
    },
    responseSummary: summarizePayload(payload),
    responseBody: bodyPreview(payload, path)
  });

  if (allow404 && response.status === 404) return null;
  if (!response.ok) {
    const message = payload?.messages?.[0]?.default_message || payload?.error || payload?.message || response.statusText;
    throw new VCenterTraceError(`vCenter ${method} ${path} failed with HTTP ${response.status}: ${message}`, trace, response.status);
  }
  return payload;
}

function soapBaseUrl(settings) {
  return normalizeBaseUrl(settings.baseUrl)
    .replace(/\/sdk\/vimService\.wsdl$/i, '')
    .replace(/\/sdk\/vimService$/i, '')
    .replace(/\/sdk$/i, '');
}

function soapEndpoint(settings) {
  return `${soapBaseUrl(settings)}/sdk`;
}

function soapWsdlEndpoint(settings) {
  return `${soapBaseUrl(settings)}/sdk/vimService.wsdl`;
}

function soapEndpointCandidates(settings) {
  return [...new Set([
    `${soapBaseUrl(settings)}/sdk`,
    `${soapBaseUrl(settings)}/sdk/`,
    `${soapBaseUrl(settings)}/sdk/vimService`
  ])];
}

function soapEnvelope(body) {
  return `<?xml version="1.0" encoding="UTF-8"?>` +
    `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vim25="urn:vim25">` +
    `<soapenv:Body>${body}</soapenv:Body></soapenv:Envelope>`;
}

function xmlEscape(value) {
  return String(value ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

function redactSoapBody(value) {
  return String(value || '')
    .replace(/(<password>)([\s\S]*?)(<\/password>)/gi, '$1[redacted]$3')
    .replace(/(<userName>)([\s\S]*?)(<\/userName>)/gi, '$1[redacted username]$3');
}

function soapBodyPreview(value) {
  const text = redactSoapBody(value);
  return text.length > MAX_TRACE_BODY_CHARS
    ? `${text.slice(0, MAX_TRACE_BODY_CHARS)}\n... truncated ${text.length - MAX_TRACE_BODY_CHARS} character(s)`
    : text;
}

function networkErrorMessage(err) {
  if (err.name === 'TimeoutError') return `Timed out after the configured request timeout.`;
  const cause = err.cause || {};
  const details = [
    err.message,
    cause.code ? `code=${cause.code}` : '',
    cause.reason ? `reason=${cause.reason}` : '',
    cause.message && cause.message !== err.message ? `cause=${cause.message}` : ''
  ].filter(Boolean);
  return details.join('; ');
}

function extractSoapTag(text, tagName) {
  const pattern = new RegExp(`<(?:[^:>]+:)?${tagName}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tagName}>`, 'i');
  const match = String(text || '').match(pattern);
  return match ? match[1].trim() : '';
}

function extractSoapBlocks(text, tagName) {
  const pattern = new RegExp(`<(?:[^:>]+:)?${tagName}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tagName}>`, 'gi');
  return [...String(text || '').matchAll(pattern)].map((match) => match[1]);
}

function decodeXml(value) {
  return String(value ?? '')
    .replace(/&apos;/g, "'")
    .replace(/&quot;/g, '"')
    .replace(/&gt;/g, '>')
    .replace(/&lt;/g, '<')
    .replace(/&amp;/g, '&');
}

function extractManagedObject(text, tagName, fallbackType = '') {
  const pattern = new RegExp(`<(?:[^:>]+:)?${tagName}([^>]*)>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tagName}>`, 'i');
  const match = String(text || '').match(pattern);
  if (!match) return { type: fallbackType, value: '' };
  const typeMatch = String(match[1] || '').match(/\btype=["']([^"']+)["']/i);
  return {
    type: typeMatch?.[1] || fallbackType,
    value: decodeXml(match[2].trim())
  };
}

function extractSoapFault(text) {
  return extractSoapTag(text, 'faultstring') || extractSoapTag(text, 'reason') || '';
}

async function probeSoapWsdl(settings, trace = null) {
  const url = soapWsdlEndpoint(settings);
  const startedAt = Date.now();
  let response;
  let text = '';
  try {
    const dispatcher = dispatcherFor(settings);
    response = await fetchForDispatcher(dispatcher)(url, {
      method: 'GET',
      headers: { Accept: 'text/xml, application/xml, */*' },
      dispatcher,
      signal: AbortSignal.timeout(settings.requestTimeoutMs)
    });
    text = await response.text();
  } catch (err) {
    pushTrace(trace, {
      method: 'GET',
      url,
      requestHeaders: { Accept: 'text/xml, application/xml, */*' },
      durationMs: Date.now() - startedAt,
      timeoutMs: settings.requestTimeoutMs,
      networkError: networkErrorMessage(err),
      diagnosticOnly: true
    });
    return;
  }
  pushTrace(trace, {
    method: 'GET',
    url,
    requestHeaders: { Accept: 'text/xml, application/xml, */*' },
    status: response.status,
    statusText: response.statusText,
    durationMs: Date.now() - startedAt,
    responseHeaders: {
      'content-type': response.headers.get('content-type') || '',
      date: response.headers.get('date') || ''
    },
    responseSummary: { type: 'wsdl', bytes: text.length, ok: response.ok },
    responseBody: soapBodyPreview(text),
    diagnosticOnly: true
  });
}

async function vSphereSoapFetch(settings, action, body, trace = null, options = {}) {
  const requestBody = soapEnvelope(body);
  const baseHeaders = {
    Accept: 'text/xml',
    'Content-Type': 'text/xml; charset=utf-8',
    SOAPAction: `"urn:vim25/${action}"`
  };
  const endpoints = options.endpoint ? [options.endpoint] : soapEndpointCandidates(settings);
  let lastError = null;
  for (const [index, url] of endpoints.entries()) {
    const headers = options.cookie ? { ...baseHeaders, Cookie: options.cookie } : baseHeaders;
    const startedAt = Date.now();
    let response;
    let text = '';
    try {
      const dispatcher = dispatcherFor(settings);
      response = await fetchForDispatcher(dispatcher)(url, {
        method: 'POST',
        headers,
        body: requestBody,
        dispatcher,
        signal: AbortSignal.timeout(settings.requestTimeoutMs)
      });
      text = await response.text();
    } catch (err) {
      const message = err.name === 'TimeoutError'
        ? `Timed out after ${settings.requestTimeoutMs} ms waiting for VMware host SOAP action ${action}.`
        : `Unable to reach VMware host SOAP action ${action}: ${networkErrorMessage(err)}`;
      pushTrace(trace, {
        method: 'POST',
        url,
        soapAction: action,
        requestHeaders: { ...headers, SOAPAction: headers.SOAPAction, Cookie: headers.Cookie ? '[redacted session cookie]' : undefined },
        requestBody: soapBodyPreview(requestBody),
        durationMs: Date.now() - startedAt,
        timeoutMs: settings.requestTimeoutMs,
        networkError: networkErrorMessage(err),
        endpointAttempt: `${index + 1} of ${endpoints.length}`
      });
      lastError = new VCenterTraceError(message, trace);
      if (index < endpoints.length - 1) {
        pushTraceInfo(trace, `Retrying VMware SOAP action ${action} with alternate ESXi endpoint path.`, {
          failedEndpoint: url,
          nextEndpoint: endpoints[index + 1]
        });
        continue;
      }
      throw lastError;
    }

    pushTrace(trace, {
      method: 'POST',
      url,
      soapAction: action,
      requestHeaders: { ...headers, SOAPAction: headers.SOAPAction, Cookie: headers.Cookie ? '[redacted session cookie]' : undefined },
      requestBody: soapBodyPreview(requestBody),
      status: response.status,
      statusText: response.statusText,
      durationMs: Date.now() - startedAt,
      responseHeaders: {
        'content-type': response.headers.get('content-type') || '',
        date: response.headers.get('date') || '',
        'set-cookie': response.headers.get('set-cookie') ? '[redacted session cookie]' : ''
      },
      responseSummary: {
        type: 'soap',
        bytes: text.length,
        fault: Boolean(extractSoapFault(text))
      },
      responseBody: soapBodyPreview(text),
      endpointAttempt: `${index + 1} of ${endpoints.length}`
    });

    const fault = extractSoapFault(text);
    if ((!response.ok || fault) && [404, 405].includes(response.status) && index < endpoints.length - 1) {
      pushTraceInfo(trace, `Retrying VMware SOAP action ${action} after HTTP ${response.status} from ESXi endpoint path.`, {
        failedEndpoint: url,
        nextEndpoint: endpoints[index + 1],
        status: response.status
      });
      continue;
    }
    if (!response.ok || fault) {
      throw new VCenterTraceError(`VMware host SOAP action ${action} failed${response.ok ? '' : ` with HTTP ${response.status}`}: ${fault || response.statusText}`, trace, response.status || 502);
    }
    if (options.capture) {
      options.capture.cookie = response.headers.get('set-cookie') || options.capture.cookie || '';
      options.capture.endpoint = url;
    }
    return text;
  }
  throw lastError || new VCenterTraceError(`VMware host SOAP action ${action} could not be completed.`, trace);
}

function unwrapList(payload) {
  if (Array.isArray(payload)) return payload;
  if (Array.isArray(payload?.value)) return payload.value;
  return [];
}

async function createSession(settings, profile, trace = null) {
  const payload = await vcenterFetch(settings, profile.sessionPath, { method: 'POST', trace });
  const sessionId = typeof payload === 'string' ? payload : payload?.value || payload?.session_id || payload?.id;
  if (!sessionId) throw new VCenterTraceError('vCenter session authentication succeeded but no session id was returned.', trace);
  return sessionId;
}

async function optionalVCenterFetch(settings, path, sessionId, trace = null) {
  try {
    return await vcenterFetch(settings, path, { sessionId, allow404: true, trace });
  } catch (err) {
    return { unavailable: true, reason: err.message };
  }
}

function firstIpFromInterfaces(interfaces = []) {
  for (const nic of interfaces) {
    const addresses = nic?.ip?.ip_addresses || nic?.ip_addresses || [];
    const firstUsable = addresses.find((entry) => {
      const address = entry?.ip_address || entry?.address || entry;
      return address && !String(address).startsWith('fe80:');
    });
    if (firstUsable) return firstUsable.ip_address || firstUsable.address || firstUsable;
  }
  return '';
}

function allIpsFromInterfaces(interfaces = []) {
  return interfaces.flatMap((nic) => {
    const addresses = nic?.ip?.ip_addresses || nic?.ip_addresses || [];
    return addresses
      .map((entry) => entry?.ip_address || entry?.address || entry)
      .filter((address) => address && !String(address).startsWith('fe80:'));
  });
}

function inferOsFamily(identity = {}) {
  const haystack = [identity.family, identity.name, localizedText(identity.full_name), identity.guest_OS].filter(Boolean).join(' ').toLowerCase();
  if (WINDOWS_HINTS.some((hint) => haystack.includes(hint))) return 'windows';
  if (LINUX_HINTS.some((hint) => haystack.includes(hint))) return 'linux';
  return 'other';
}

function localizedText(value) {
  if (!value) return '';
  if (typeof value === 'string') return value;
  if (typeof value === 'object') return value.default_message || value.id || '';
  return String(value);
}

function normalizePowerState(value) {
  const text = String(value || '').trim();
  const map = {
    poweredon: 'POWERED_ON',
    poweredoff: 'POWERED_OFF',
    suspended: 'SUSPENDED'
  };
  return map[text.toLowerCase()] || text;
}

export function normalizeVCenterVm(vm, identity = null, interfacesPayload = null) {
  const interfaces = unwrapList(interfacesPayload);
  const interfaceIp = firstIpFromInterfaces(interfaces);
  const identityIp = identity?.ip_address || identity?.ipAddress || '';
  const fqdn = identity?.host_name || identity?.hostName || '';
  const name = vm.name || fqdn || vm.vm || vm.id;
  const address = fqdn || identityIp || interfaceIp || name;
  const ipAddresses = [...new Set([identityIp, interfaceIp, ...allIpsFromInterfaces(interfaces)].filter(Boolean))];

  return {
    id: vm.vm || vm.id,
    name,
    fqdn,
    address,
    ipAddress: ipAddresses[0] || '',
    ipAddresses,
    powerState: normalizePowerState(vm.power_state || vm.powerState || ''),
    osFamily: inferOsFamily(identity || {}),
    guestFullName: localizedText(identity?.full_name) || identity?.name || '',
    toolsAvailable: Boolean(identity || interfaces.length),
    source: 'vcenter'
  };
}

function normalizeVCenterSummaryVm(vm) {
  return {
    id: vm.vm || vm.id,
    name: vm.name || vm.vm || vm.id || '',
    fqdn: '',
    address: vm.name || vm.vm || vm.id || '',
    ipAddress: '',
    ipAddresses: [],
    powerState: normalizePowerState(vm.power_state || vm.powerState || ''),
    osFamily: 'other',
    guestFullName: '',
    toolsAvailable: false,
    source: 'vcenter'
  };
}

function normalizeStandaloneVmObject(object, connection = null) {
  const props = object.props || {};
  const guestHostName = props['guest.hostName'] || props['summary.guest.hostName'] || '';
  const guestIp = props['guest.ipAddress'] || props['summary.guest.ipAddress'] || '';
  const guestFullName = props['guest.guestFullName'] || props['summary.config.guestFullName'] || '';
  const guestId = props['guest.guestId'] || props['summary.config.guestId'] || '';
  const name = props.name || guestHostName || object.id;
  return {
    id: object.id,
    name,
    fqdn: guestHostName,
    address: guestHostName || guestIp || name,
    ipAddress: guestIp,
    ipAddresses: guestIp ? [guestIp] : [],
    powerState: normalizePowerState(props['runtime.powerState'] || props['summary.runtime.powerState'] || ''),
    osFamily: inferOsFamily({ full_name: guestFullName, guest_OS: guestId, host_name: guestHostName }),
    guestFullName,
    toolsAvailable: Boolean(guestHostName || guestIp || guestFullName || guestId),
    source: 'standalone-vm',
    sourceConnectionId: connection?.id || null,
    sourceConnectionName: connection?.name || '',
    targetType: 'host'
  };
}

export function vmMatchesFilter(candidate, filter = {}) {
  const operator = filter.operator || 'contains';
  const needle = String(filter.value || '').trim().toLowerCase();
  if (!needle) return true;
  const field = filter.field || 'hostname';
  if (field === 'ip') {
    const values = [candidate.ipAddress, ...(candidate.ipAddresses || [])]
      .filter(Boolean)
      .map((value) => String(value).toLowerCase());
    if (operator === 'equals') return values.some((value) => value === needle);
    if (operator === 'startsWith') return values.some((value) => value.startsWith(needle));
    if (operator === 'endsWith') return values.some((value) => value.endsWith(needle));
    return values.some((value) => value.includes(needle));
  }
  const haystacks = {
    name: [candidate.name],
    hostname: [candidate.name, candidate.fqdn, candidate.address].filter(Boolean),
    fqdn: [candidate.fqdn, candidate.address].filter(Boolean),
  }[field] || [candidate.name];
  const values = haystacks.map((value) => String(value || '').toLowerCase());
  if (operator === 'equals') return values.some((value) => value === needle);
  if (operator === 'startsWith') return values.some((value) => value.startsWith(needle));
  if (operator === 'endsWith') return values.some((value) => value.endsWith(needle));
  return values.some((value) => value.includes(needle));
}

export function filterSummaryVms(vms = [], filter = {}) {
  const field = filter?.field || 'hostname';
  if (!['name', 'hostname'].includes(field)) return { vms, summaryMatched: vms.length, summaryFilterApplied: false, summaryFilterFallback: false };
  const summaryFilter = { ...filter, field: 'name' };
  const matched = vms.filter((vm) => vmMatchesFilter(normalizeVCenterSummaryVm(vm), summaryFilter));
  if (field === 'hostname') {
    const matchedIds = new Set(matched.map((vm) => vm.vm || vm.id));
    return {
      vms: [...matched, ...vms.filter((vm) => !matchedIds.has(vm.vm || vm.id))],
      summaryMatched: matched.length,
      summaryFilterApplied: true,
      summaryFilterFallback: matched.length === 0
    };
  }
  return { vms: matched, summaryMatched: matched.length, summaryFilterApplied: true, summaryFilterFallback: false };
}

function canUseServerSideNameFilter(filter = {}) {
  return filter.field === 'name' && filter.operator === 'equals' && String(filter.value || '').trim();
}

export function buildVmListPath(profile, filter = {}) {
  if (!canUseServerSideNameFilter(filter)) return profile.vmListPath;
  const params = new URLSearchParams();
  params.append(profile.vmNameParam, String(filter.value).trim());
  return `${profile.vmListPath}?${params.toString()}`;
}

export function buildHostPayloadFromVm(candidate, options = {}) {
  if (candidate.source === 'standalone-host') return buildHostPayloadFromStandaloneHost(candidate, options);
  if (candidate.source === 'standalone-vm') return buildHostPayloadFromStandaloneVm(candidate, options);
  const tags = [...new Set(['vmware', 'vcenter', `vcenter:${candidate.id}`, ...(options.tags || [])].filter(Boolean))];
  return {
    name: candidate.name,
    fqdn: candidate.fqdn || '',
    address: candidate.address || candidate.ipAddress || candidate.name,
    osFamily: normalizeOsFamily(candidate.osFamily, 'other'),
    transport: options.transport || 'winrm',
    port: options.port || null,
    credentialId: options.credentialId || null,
    tags,
    notes: [
      `Imported from VMware vCenter VM ${candidate.id}.`,
      candidate.powerState ? `Power state: ${candidate.powerState}.` : '',
      candidate.guestFullName ? `Guest OS: ${candidate.guestFullName}.` : '',
      candidate.ipAddresses?.length ? `IP addresses: ${candidate.ipAddresses.join(', ')}.` : 'IP address unavailable from VMware Tools at import time.'
    ].filter(Boolean).join(' '),
    visibility: options.visibility || 'shared',
    groupId: options.visibility === 'group' ? options.groupId || null : null,
    sourceType: 'vmware',
    sourceConnectionId: options.connectionId || null,
    sourceRef: candidate.id || ''
  };
}

export function buildHostPayloadFromStandaloneVm(candidate, options = {}) {
  const tags = [...new Set([
    'vmware',
    'esxi',
    'standalone-esxi',
    candidate.id ? `esxi-vm:${candidate.id}` : '',
    ...(options.tags || [])
  ].filter(Boolean))];
  return {
    name: candidate.name,
    fqdn: candidate.fqdn || '',
    address: candidate.address || candidate.ipAddress || candidate.name,
    osFamily: normalizeOsFamily(candidate.osFamily, 'other'),
    transport: options.transport || 'winrm',
    port: options.port || null,
    credentialId: options.credentialId || null,
    tags,
    notes: [
      `Imported from standalone VMware ESXi VM ${candidate.id}.`,
      candidate.sourceConnectionName || candidate.sourceConnectionId
        ? `Source ESXi connection: ${candidate.sourceConnectionName || candidate.sourceConnectionId}.`
        : '',
      candidate.powerState ? `Power state: ${candidate.powerState}.` : '',
      candidate.guestFullName ? `Guest OS: ${candidate.guestFullName}.` : '',
      candidate.ipAddresses?.length ? `IP addresses: ${candidate.ipAddresses.join(', ')}.` : 'IP address unavailable from VMware Tools at import time.'
    ].filter(Boolean).join(' '),
    visibility: options.visibility || 'shared',
    groupId: options.visibility === 'group' ? options.groupId || null : null,
    sourceType: 'vmware',
    sourceConnectionId: candidate.sourceConnectionId || options.connectionId || null,
    sourceRef: candidate.id || ''
  };
}

export function buildStandaloneHostCandidate(connection, settings = settingsFromVCenterConnection(connection)) {
  const hostname = hostnameFromBaseUrl(settings.baseUrl);
  const name = connection?.name || hostname || settings.baseUrl;
  return {
    id: `standalone-host:${connection?.id || hostname || settings.baseUrl}`,
    name,
    fqdn: hostname,
    address: hostname || settings.baseUrl,
    ipAddress: '',
    ipAddresses: [],
    powerState: 'Standalone host',
    osFamily: 'other',
    guestFullName: 'Standalone VMware ESXi host',
    toolsAvailable: false,
    source: 'standalone-host',
    sourceConnectionId: connection?.id || null,
    sourceConnectionName: connection?.name || '',
    targetType: 'host'
  };
}

export function buildHostPayloadFromStandaloneHost(candidate, options = {}) {
  const tags = [...new Set([
    'vmware',
    'esxi',
    'standalone-esxi',
    candidate.sourceConnectionId ? `vmware-host:${candidate.sourceConnectionId}` : '',
    ...(options.tags || [])
  ].filter(Boolean))];
  return {
    name: candidate.name,
    fqdn: candidate.fqdn || '',
    address: candidate.address || candidate.fqdn || candidate.name,
    osFamily: normalizeOsFamily(candidate.osFamily, 'other'),
    transport: options.transport || 'ssh',
    port: options.port || null,
    credentialId: options.credentialId || null,
    tags,
    notes: [
      `Imported from standalone VMware ESXi connection ${candidate.sourceConnectionName || candidate.sourceConnectionId || candidate.name}.`,
      'This host was added from a direct VMware host connection, not vCenter VM inventory.'
    ].filter(Boolean).join(' '),
    visibility: options.visibility || 'shared',
    groupId: options.visibility === 'group' ? options.groupId || null : null,
    sourceType: 'vmware',
    sourceConnectionId: candidate.sourceConnectionId || options.connectionId || null,
    sourceRef: 'standalone-esxi-host'
  };
}

function hostnameFromBaseUrl(value) {
  try {
    return new URL(value).hostname;
  } catch {
    return String(value || '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '');
  }
}

function soapRefElement(name, ref, fallbackType) {
  return `<${name} type="${xmlEscape(ref?.type || fallbackType)}">${xmlEscape(ref?.value || '')}</${name}>`;
}

async function connectStandaloneSoap(settings, trace = null) {
  pushTraceInfo(trace, `Using ${WEB_SERVICES_PROFILE_LABEL}`, {
    mode: 'web-services',
    endpoint: soapEndpoint(settings),
    endpointCandidates: soapEndpointCandidates(settings),
    wsdl: soapWsdlEndpoint(settings)
  });
  await probeSoapWsdl(settings, trace);
  const serviceContent = await vSphereSoapFetch(
    settings,
    'RetrieveServiceContent',
    `<RetrieveServiceContent xmlns="urn:vim25"><_this type="ServiceInstance">ServiceInstance</_this></RetrieveServiceContent>`,
    trace
  );
  const refs = {
    rootFolder: extractManagedObject(serviceContent, 'rootFolder', 'Folder'),
    propertyCollector: extractManagedObject(serviceContent, 'propertyCollector', 'PropertyCollector'),
    viewManager: extractManagedObject(serviceContent, 'viewManager', 'ViewManager'),
    sessionManager: extractManagedObject(serviceContent, 'sessionManager', 'SessionManager')
  };
  const loginMeta = {};
  await vSphereSoapFetch(
    settings,
    'Login',
    `<Login xmlns="urn:vim25">${soapRefElement('_this', refs.sessionManager, 'SessionManager')}<userName>${xmlEscape(settings.username)}</userName><password>${xmlEscape(settings.password)}</password></Login>`,
    trace,
    { capture: loginMeta }
  );
  return { ...refs, sessionCookie: loginMeta.cookie || '', endpoint: loginMeta.endpoint || '' };
}

async function createVmContainerView(settings, refs, trace = null) {
  const response = await vSphereSoapFetch(
    settings,
    'CreateContainerView',
    `<CreateContainerView xmlns="urn:vim25">${soapRefElement('_this', refs.viewManager, 'ViewManager')}${soapRefElement('container', refs.rootFolder, 'Folder')}<type>VirtualMachine</type><recursive>true</recursive></CreateContainerView>`,
    trace,
    { cookie: refs.sessionCookie, endpoint: refs.endpoint }
  );
  return extractManagedObject(response, 'returnval', 'ContainerView');
}

function retrievePropertiesBody(propertyCollector, viewRef, limit = 100) {
  const paths = [
    'name',
    'guest.hostName',
    'guest.ipAddress',
    'guest.guestFullName',
    'guest.guestId',
    'runtime.powerState',
    'summary.guest.hostName',
    'summary.guest.ipAddress',
    'summary.config.guestFullName',
    'summary.config.guestId',
    'summary.runtime.powerState'
  ];
  return `<RetrievePropertiesEx xmlns="urn:vim25" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">` +
    `${soapRefElement('_this', propertyCollector, 'PropertyCollector')}` +
    `<specSet>` +
    `<propSet><type>VirtualMachine</type><all>false</all>${paths.map((pathSet) => `<pathSet>${pathSet}</pathSet>`).join('')}</propSet>` +
    `<objectSet>${soapRefElement('obj', viewRef, 'ContainerView')}<skip>true</skip>` +
    `<selectSet xsi:type="TraversalSpec"><name>containerViewTraversal</name><type>ContainerView</type><path>view</path><skip>false</skip></selectSet>` +
    `</objectSet>` +
    `</specSet>` +
    `<options><maxObjects>${Math.max(1, Math.min(Number(limit) || 100, 500))}</maxObjects></options>` +
    `</RetrievePropertiesEx>`;
}

function continuePropertiesBody(propertyCollector, token) {
  return `<ContinueRetrievePropertiesEx xmlns="urn:vim25">${soapRefElement('_this', propertyCollector, 'PropertyCollector')}<token>${xmlEscape(token)}</token></ContinueRetrievePropertiesEx>`;
}

function parseVmPropertyObjects(xml) {
  return extractSoapBlocks(xml, 'objects').map((block) => {
    const obj = extractManagedObject(block, 'obj', 'VirtualMachine');
    const props = {};
    for (const propBlock of extractSoapBlocks(block, 'propSet')) {
      const name = decodeXml(extractSoapTag(propBlock, 'name'));
      const rawValue = extractSoapTag(propBlock, 'val');
      if (name) props[name] = decodeXml(rawValue.replace(/<[^>]+>/g, '').trim());
    }
    return { id: obj.value, type: obj.type, props };
  }).filter((object) => object.id);
}

async function retrieveStandaloneVmInventory(settings, { connection = null, limit = 100, trace = null } = {}) {
  const refs = await connectStandaloneSoap(settings, trace);
  const viewRef = await createVmContainerView(settings, refs, trace);
  const objects = [];
  let response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', retrievePropertiesBody(refs.propertyCollector, viewRef, limit), trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
  objects.push(...parseVmPropertyObjects(response));
  let token = decodeXml(extractSoapTag(response, 'token'));
  while (token && objects.length < limit) {
    response = await vSphereSoapFetch(settings, 'ContinueRetrievePropertiesEx', continuePropertiesBody(refs.propertyCollector, token), trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
    objects.push(...parseVmPropertyObjects(response));
    token = decodeXml(extractSoapTag(response, 'token'));
  }
  return objects.slice(0, limit).map((object) => normalizeStandaloneVmObject(object, connection));
}

async function retrieveStandaloneVmPowerState(settings, vmId, trace = null) {
  const refs = await connectStandaloneSoap(settings, trace);
  const vmRef = { type: 'VirtualMachine', value: vmId };
  const body = `<RetrievePropertiesEx xmlns="urn:vim25" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">` +
    `${soapRefElement('_this', refs.propertyCollector, 'PropertyCollector')}` +
    `<specSet><propSet><type>VirtualMachine</type><all>false><pathSet>runtime.powerState</pathSet></propSet>` +
    `<objectSet>${soapRefElement('obj', vmRef, 'VirtualMachine')}<skip>false</skip></objectSet></specSet>` +
    `<options><maxObjects>1</maxObjects></options></RetrievePropertiesEx>`;
  const response = await vSphereSoapFetch(settings, 'RetrievePropertiesEx', body, trace, { cookie: refs.sessionCookie, endpoint: refs.endpoint });
  const object = parseVmPropertyObjects(response)[0];
  return normalizePowerState(object?.props?.['runtime.powerState'] || '');
}

export async function previewVCenterHosts(options = {}) {
  const trace = [];
  const settings = settingsFromVCenterConnection(options.connection);
  try {
    assertConfigured(settings);
  } catch (err) {
    throw new VCenterTraceError(err.message, trace, 400);
  }
  if (settings.targetType === 'host') {
    const allVms = await retrieveStandaloneVmInventory(settings, {
      connection: options.connection,
      limit: options.limit || 100,
      trace
    });
    const filtered = allVms.filter((candidate) => vmMatchesFilter(candidate, options.filter)).slice(0, options.limit || 100);
    pushTraceInfo(trace, 'Enumerated standalone ESXi VM inventory through the vSphere Web Services API.', {
      connectionId: options.connection?.id || null,
      baseUrl: settings.baseUrl,
      totalFromHost: allVms.length,
      matched: filtered.length
    });
    return {
      status: vcenterConnectionStatus(options.connection),
      filter: options.filter || {},
      count: filtered.length,
      diagnostics: {
        targetType: 'host',
        apiMode: 'web-services',
        apiProfile: WEB_SERVICES_PROFILE_LABEL,
        totalFromVCenter: allVms.length,
        summaryMatched: filtered.length,
        scanned: allVms.length,
        resultLimit: options.limit || 100,
        sampleNames: allVms.slice(0, 12).map((vm) => vm.name || vm.id).filter(Boolean),
        standaloneHost: true,
        filter: options.filter || {}
      },
      trace,
      candidates: filtered
    };
  }
  const { profile, sessionId, vmPayload } = await connectAndListVms(settings, options.filter, trace);
  const allVms = unwrapList(vmPayload);
  const summaryFilter = filterSummaryVms(allVms, options.filter);
  const vms = summaryFilter.vms;
  const candidates = [];
  const diagnostics = {
    totalFromVCenter: allVms.length,
    scanned: 0,
    summaryMatched: summaryFilter.summaryMatched,
    summaryFilterApplied: summaryFilter.summaryFilterApplied,
    summaryFilterFallback: summaryFilter.summaryFilterFallback,
    apiMode: profile.mode,
    apiProfile: profile.label,
    serverSideNameFilter: canUseServerSideNameFilter(options.filter),
    resultLimit: options.limit || 100,
    sampleNames: allVms.slice(0, 12).map((vm) => vm.name || vm.vm || vm.id).filter(Boolean),
    filter: options.filter || {}
  };

  await enrichVmsForPreview({
    vms,
    settings,
    profile,
    sessionId,
    trace,
    filter: options.filter,
    limit: options.limit || 100,
    diagnostics,
    candidates
  });

  return {
    status: options.connection ? vcenterConnectionStatus(options.connection) : vcenterStatus(),
    filter: options.filter || {},
    count: candidates.length,
    diagnostics,
    trace,
    candidates
  };
}

export async function testVCenterConnection(connection) {
  const trace = [];
  const settings = settingsFromVCenterConnection(connection);
  assertConfigured(settings);
  if (settings.targetType === 'host') return testStandaloneHostConnection(connection, settings, trace);
  const { profile, vmPayload } = await connectAndListVms(settings, {}, trace);
  return {
    status: vcenterConnectionStatus(connection),
    apiProfile: profile.label,
    count: unwrapList(vmPayload).length,
    trace,
    message: `Connected to ${settings.baseUrl} with ${profile.label}; ${unwrapList(vmPayload).length} VM summary row(s) returned.`
  };
}

export async function testStandaloneHostConnection(connection, settings = settingsFromVCenterConnection(connection), trace = []) {
  const vms = await retrieveStandaloneVmInventory(settings, {
    connection,
    limit: 1,
    trace
  });
  return {
    status: vcenterConnectionStatus(connection),
    apiProfile: WEB_SERVICES_PROFILE_LABEL,
    count: vms.length,
    trace,
    message: `Connected to standalone VMware host ${settings.baseUrl} with ${WEB_SERVICES_PROFILE_LABEL}; VM inventory query returned ${vms.length} row(s).`
  };
}

export async function getVCenterVmPowerState(connection, vmId, trace = null) {
  if (!connection || !vmId) return { checked: false, reason: 'Missing vCenter connection or VM reference.' };
  const settings = settingsFromVCenterConnection(connection);
  if (settings.targetType === 'host') {
    assertConfigured(settings);
    const state = await retrieveStandaloneVmPowerState(settings, vmId, trace);
    return {
      checked: Boolean(state),
      state,
      apiProfile: WEB_SERVICES_PROFILE_LABEL,
      reason: state ? '' : 'VM power state was not returned by the ESXi Web Services API.'
    };
  }
  assertConfigured(settings);
  const modes = settings.apiMode === 'auto' ? ['api', 'rest'] : [settings.apiMode];
  let lastError = null;
  for (const mode of modes) {
    const profile = API_PROFILES[mode];
    try {
      const sessionId = await createSession(settings, profile, trace);
      const payload = await vcenterFetch(settings, profile.vmDetailPath(vmId), { sessionId, trace });
      const value = payload?.value || payload || {};
      return {
        checked: true,
        state: value.power_state || value.powerState || '',
        apiProfile: profile.label
      };
    } catch (err) {
      lastError = err;
      if (settings.apiMode !== 'auto' || mode === modes.at(-1) || !shouldTryFallback(err)) throw err;
      pushTraceInfo(trace, `Falling back after ${profile.label} power-state lookup failed`, {
        mode,
        error: err.message,
        statusCode: err.statusCode
      });
    }
  }
  throw lastError || new VCenterTraceError('No vCenter API profile could be attempted for power-state lookup.', trace);
}

async function enrichVmsForPreview({ vms, settings, profile, sessionId, trace, filter, limit, diagnostics, candidates }) {
  let index = 0;
  async function worker() {
    while (index < vms.length && candidates.length < limit) {
      const vm = vms[index++];
      const vmId = vm.vm || vm.id;
      if (!vmId) continue;
      diagnostics.scanned += 1;
      const identity = await optionalVCenterFetch(settings, profile.guestIdentityPath(vmId), sessionId, trace);
      const candidateWithoutNetworking = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, null);
      if (!vmMatchesFilter(candidateWithoutNetworking, filter)) continue;
      const interfaces = await optionalVCenterFetch(settings, profile.guestNetworkingPath(vmId), sessionId, trace);
      const candidate = normalizeVCenterVm(vm, identity?.unavailable ? null : identity, interfaces?.unavailable ? null : interfaces);
      if (candidates.length < limit) candidates.push(candidate);
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(VM_ENRICHMENT_CONCURRENCY, Math.max(vms.length, 1)) }, () => worker())
  );
}

function shouldTryFallback(err) {
  return err instanceof VCenterTraceError && [404, 405, 501].includes(err.statusCode);
}

async function connectAndListWithProfile(settings, filter, profile, trace) {
  pushTraceInfo(trace, `Using ${profile.label}`, {
    mode: profile.mode,
    sessionPath: profile.sessionPath,
    vmListPath: buildVmListPath(profile, filter)
  });
  const sessionId = await createSession(settings, profile, trace);
  const vmPayload = await vcenterFetch(settings, buildVmListPath(profile, filter), { sessionId, trace });
  return { profile, sessionId, vmPayload };
}

async function connectAndListVms(settings, filter, trace) {
  const modes = settings.apiMode === 'auto' ? ['api', 'rest'] : [settings.apiMode];
  let lastError = null;
  for (const mode of modes) {
    try {
      return await connectAndListWithProfile(settings, filter, API_PROFILES[mode], trace);
    } catch (err) {
      lastError = err;
      if (settings.apiMode !== 'auto' || mode === modes.at(-1) || !shouldTryFallback(err)) throw err;
      pushTraceInfo(trace, `Falling back after ${API_PROFILES[mode].label} failed`, {
        mode,
        error: err.message,
        statusCode: err.statusCode
      });
    }
  }
  throw lastError || new VCenterTraceError('No vCenter API profile could be attempted.', trace);
}
