// Pure, framework-free helpers for the GE-Enforce manifest editor.
//
// ManifestEditor.vue imports these directly (the component no longer keeps its
// own copies), so the unit tests in entryForm.spec.js exercise the shipped
// code path. Change the editor logic HERE.
//
// Everything here is a plain function of its inputs. No Vue, no reactivity,
// no network. That is the whole point - deterministic logic we can pin down.

export const ENTRY_TYPES = ['MSI', 'EXE', 'CMD', 'BAT', 'PS1', 'INF', 'File', 'Registry']
export const REG_TYPES = ['String', 'DWord', 'QWord', 'MultiString', 'ExpandString', 'Binary']
export const DETECTION_METHODS = ['Registry', 'File', 'FileVersion', 'Hash', 'MarkerFile',
  'ValueMatches', 'pnputil', 'Always']
export const INUSE_BEHAVIORS = ['Defer', 'CloseAndReopen', 'ForceClose', 'ScheduleForReboot']

// Plain-language description of each detection method, shown under the Detection
// method dropdown so a first-time site admin understands what "present" means
// for the method they picked. Key '' is the no-detection case.
export const DETECTION_METHOD_HINTS = {
  '': 'No detection rule: the action runs every cycle.',
  Registry: 'Already correct if the registry value at Detection path/name exists (and equals Detection value when one is set).',
  File: 'Already correct if the file at Detection path exists.',
  FileVersion: 'Already correct if the file at Detection path is at Detection value or newer. This target feeds the Compliance panel.',
  Hash: 'Already correct if the file at Detection path matches the SHA256 in Detection value. Re-copies when the file changed.',
  MarkerFile: 'Already correct if the marker file at Detection path exists. Installs once, then the marker suppresses reruns.',
  ValueMatches: 'Already correct if the registry value at Detection path/name equals Detection value exactly.',
  pnputil: 'Already correct if a driver matching Detection pattern is staged in the Windows driver store. For INF entries.',
  Always: 'Never counts as present, so the action runs every cycle. Same effect as no detection rule.',
}

// Description for the currently selected detection method, or '' if unknown.
export function detectionMethodHint(method) {
  return DETECTION_METHOD_HINTS[method || ''] || ''
}

// One blank entry form, matching the shape ManifestEditor seeds for a new entry.
export function blankEntry() {
  return { Type: 'MSI', DetectionMethod: '', RegType: 'String',
    inuseBehavior: '', inuseProcesses: [], appid: null,
    PreEnrollment: false, KillAfterDetection: false, PCTypesStrict: false }
}

// Split a comma list into trimmed non-empty parts.
export function splitList(value) {
  return (value || '').split(',').map(item => item.trim()).filter(Boolean)
}

// Turn an edit-form object into the API body sent to create/update an entry.
//
// Rules that matter (and that the specs pin):
// - appid is always present (null unlinks). It is shopdb metadata; the backend
//   keeps it OFF the manifest JSON, so it is NOT one of the manifest scalars.
// - empty/undefined/null scalars are dropped.
// - Registry DWord/QWord values become real numbers; other reg types stay strings.
// - comma lists become arrays, and are dropped when empty.
// - preinstall flags only appear when truthy.
// - InUseCheck carries per-process Name, optional ExePath, optional numeric timeout.
export function buildEntryPayload(form) {
  // appid is shopdb metadata, always sent (null unlinks); the backend keeps it
  // off the manifest JSON.
  const out = { Name: form.Name, Type: form.Type, appid: form.appid ?? null }
  const scalars = ['Installer', 'InstallArgs', 'Script', 'Args', 'Source',
    'Destination', 'RegPath', 'RegName', 'RegType', 'DetectionPath',
    'DetectionName', 'DetectionValue', 'DetectionPattern', '_CmmVersion',
    'LogFile', 'ApplyMode', 'UpdateWindow', '_comment']
  for (const key of scalars) {
    if (form[key] !== undefined && form[key] !== '' && form[key] !== null) out[key] = form[key]
  }
  if (form.DetectionMethod) out.DetectionMethod = form.DetectionMethod
  if (form.WaitTimeoutSec) out.WaitTimeoutSec = form.WaitTimeoutSec
  if (form.Type === 'Registry' && form.RegValue !== undefined && form.RegValue !== '') {
    out.RegValue = ['DWord', 'QWord'].includes(form.RegType) ? Number(form.RegValue) : form.RegValue
  }
  const pctypes = splitList(form.PCTypes)
  if (pctypes.length) out.PCTypes = pctypes
  const hostnames = splitList(form.TargetHostnames)
  if (hostnames.length) out.TargetHostnames = hostnames
  const machinenumbers = splitList(form.TargetMachineNumbers)
  if (machinenumbers.length) out.TargetMachineNumbers = machinenumbers
  for (const flag of ['PreEnrollment', 'KillAfterDetection', 'PCTypesStrict']) {
    if (form[flag]) out[flag] = true
  }
  if (form.inuseBehavior) {
    out.InUseCheck = {
      Behavior: form.inuseBehavior,
      Processes: (form.inuseProcesses || []).filter(process => process.name).map(process => {
        const processData = { Name: process.name }
        if (process.exepath) processData.ExePath = process.exepath
        if (process.timeout !== null && process.timeout !== '' && process.timeout !== undefined) {
          processData.GracefulCloseTimeoutSec = Number(process.timeout)
        }
        return processData
      }),
    }
  }
  return out
}

// Which entry types the type dropdown offers, given the scope phase. Preinstall
// supports MSI/EXE only, but keeps a current out-of-set value visible.
export function availableEntryTypes(isPreinstall, currentType) {
  if (!isPreinstall) return ENTRY_TYPES
  const allowed = ['MSI', 'EXE']
  return currentType && !allowed.includes(currentType) ? [...allowed, currentType] : allowed
}

// Which detection methods the dropdown offers. Preinstall supports Registry/File
// only, but keeps a current out-of-set value visible.
export function availableDetectionMethods(isPreinstall, currentMethod) {
  if (!isPreinstall) return DETECTION_METHODS
  const allowed = ['Registry', 'File']
  return currentMethod && !allowed.includes(currentMethod) ? [...allowed, currentMethod] : allowed
}

// Which targeting gates a scope actually surfaces by default (before "Show all").
export function targetingGates(scope) {
  if (!scope) {
    return { pctypes: true, cmmversion: false, machinenumbers: false, hostnames: false }
  }
  const name = (scope.scopename || '').toLowerCase()
  const entries = scope.entries || []
  const fleetwide = scope.iscommon || scope.phase === 'preinstall' || name === 'common'
  return {
    pctypes: fleetwide || entries.some(e => (e.PCTypes || []).length),
    cmmversion: /cmm/.test(name) || entries.some(e => e._CmmVersion),
    // Data-driven (a loose name match would wrongly flag 'nocollections').
    machinenumbers: entries.some(e => (e.TargetMachineNumbers || []).length),
    hostnames: entries.some(e => (e.TargetHostnames || []).length),
  }
}

// The contextual hint under the Targeting header.
export function targetingHint(scope) {
  if (!scope) return ''
  if (scope.iscommon || scope.scopename === 'common') {
    return 'Fleet-wide manifest: use PC types to target which types get this entry.'
  }
  if (scope.phase === 'preinstall') {
    return 'Preinstall manifest: use PC types to target; preinstall flags apply here.'
  }
  return `This manifest already runs only on ${scope.scopename} PCs. `
    + 'Targeting below narrows within that (version, bay, or subtype).'
}

// One-line summary of what a scope installs (installer entries only).
export function scopeSummary(scope) {
  if (!scope || !scope.entries || !scope.entries.length) return ''
  const installers = scope.entries
    .filter(e => ['MSI', 'EXE', 'CMD', 'BAT'].includes(e.Type))
    .map(e => e.Name)
  const shown = installers.slice(0, 6).join(', ')
  const more = installers.length > 6 ? `, +${installers.length - 6} more` : ''
  const apps = installers.length ? `Installs ${shown}${more}. ` : ''
  return `${apps}${scope.entries.length} entries; runs after common.`
}

// Verb table for describeEntry.
export const ACTION_VERB = {
  MSI: 'Installs', EXE: 'Installs', CMD: 'Runs', BAT: 'Runs',
  PS1: 'Runs a script for', INF: 'Installs a driver for',
  File: 'Copies a file for', Registry: 'Sets a registry value for',
}

// Plain-English one-liner: what an entry does, when it self-heals, who it hits.
export function describeEntry(entry) {
  let text = `${ACTION_VERB[entry.Type] || 'Applies'} ${entry.Name}`
  const method = entry.DetectionMethod
  if (!method || method === 'Always') text += '; runs every cycle'
  else if (method === 'FileVersion') text += `; reinstalls unless version is ${entry.DetectionValue || 'set'}`
  else if (method === 'Hash') text += '; re-copies if the file changed'
  else if (method === 'MarkerFile') text += '; installs once'
  else text += '; reinstalls if not detected'
  if (entry._CmmVersion) text += `; CMM ${entry._CmmVersion} bays only`
  else if (entry.TargetMachineNumbers && entry.TargetMachineNumbers.length) {
    text += `; ${entry.TargetMachineNumbers.length} specific bay(s)`
  } else if (entry.TargetHostnames && entry.TargetHostnames.length) {
    text += '; specific hostname(s)'
  } else if (entry.PCTypes && entry.PCTypes.length) {
    text += `; ${entry.PCTypes.length} PC type(s)`
  }
  if (entry.appname) text += `; tracked: ${entry.appname}`
  return text
}
