// Pure mapping from a POSHManager Intune deployment plan (+ optional PSADT
// profile) to the Microsoft Graph `win32LobApp` resource shape and its
// assignment/relationship bodies.
//
// These functions perform no I/O so they can be unit-tested without a tenant.
// The live HTTP calls live in graphService; the publish controller composes the
// two. Field shapes follow the Graph beta `win32LobApp` documentation.

const RESTART_BEHAVIOR = {
  'return-code': 'basedOnReturnCode',
  suppress: 'suppress',
  intune: 'force'
};

const RETURN_CODE_TYPES = new Set(['success', 'softReboot', 'hardReboot', 'retry', 'failed']);

// Map a human OS string ("Windows 10 22H2", "Windows 11 23H2") to the Graph
// windowsMinimumOperatingSystem boolean flag. Defaults to a safe broad floor.
function minimumSupportedOperatingSystem(minOs = '') {
  const text = String(minOs).toLowerCase();
  const flags = {
    v10_1607: false, v10_1703: false, v10_1709: false, v10_1803: false,
    v10_1809: false, v10_1903: false, v10_1909: false, v10_2004: false,
    v10_20H2: false, v10_21H1: false, v10_21H2: false, v10_22H2: false,
    v11_21H2: false, v11_22H2: false, v11_23H2: false, v11_24H2: false
  };
  const map = [
    ['11 24h2', 'v11_24H2'], ['11 23h2', 'v11_23H2'], ['11 22h2', 'v11_22H2'], ['11 21h2', 'v11_21H2'],
    ['22h2', 'v10_22H2'], ['21h2', 'v10_21H2'], ['21h1', 'v10_21H1'], ['20h2', 'v10_20H2'],
    ['2004', 'v10_2004'], ['1909', 'v10_1909'], ['1903', 'v10_1903'], ['1809', 'v10_1809'],
    ['1803', 'v10_1803'], ['1709', 'v10_1709'], ['1703', 'v10_1703'], ['1607', 'v10_1607']
  ];
  const match = map.find(([needle]) => text.includes(needle));
  flags[match ? match[1] : 'v10_1809'] = true;
  return flags;
}

function applicableArchitectures(architecture = 'x64') {
  if (architecture === 'both') return 'x64,x86';
  if (architecture === 'x86') return 'x86';
  return 'x64';
}

function mapReturnCodes(returnCodes = []) {
  const mapped = (returnCodes || []).map((entry) => ({
    returnCode: Number(entry.code),
    type: RETURN_CODE_TYPES.has(entry.type) ? entry.type : 'failed'
  })).filter((entry) => Number.isFinite(entry.returnCode));
  // Intune requires at least the canonical 0/1707/3010/1641 floor; ensure 0=success exists.
  if (!mapped.some((entry) => entry.returnCode === 0)) mapped.unshift({ returnCode: 0, type: 'success' });
  return mapped;
}

// Detection rule mapping. Our model stores a single `detectionRule` string plus
// a `detectionType`, so structured types (file/registry) that need more fields
// are emitted best-effort and a warning is returned for operator review.
function mapDetectionRules(deployment, warnings) {
  const rule = String(deployment.detectionRule || '').trim();
  switch (deployment.detectionType) {
    case 'msi-product-code':
      if (!rule) warnings.push('MSI detection selected but no product code provided.');
      return [{
        '@odata.type': '#microsoft.graph.win32LobAppProductCodeDetection',
        productCode: rule,
        productVersionOperator: 'notConfigured',
        productVersion: null
      }];
    case 'custom-script':
      if (!rule) warnings.push('Custom-script detection selected but the script body is empty.');
      return [{
        '@odata.type': '#microsoft.graph.win32LobAppPowerShellScriptDetection',
        scriptContent: Buffer.from(rule, 'utf8').toString('base64'),
        enforceSignatureCheck: false,
        runAs32Bit: false
      }];
    case 'file-version':
      warnings.push('File-version detection requires structured path/file/version fields; emitted a best-effort rule for review.');
      return [{
        '@odata.type': '#microsoft.graph.win32LobAppFileSystemDetection',
        path: rule || 'C:\\Program Files',
        fileOrFolderName: '',
        check32BitOn64System: false,
        detectionType: 'exists',
        operator: 'notConfigured',
        detectionValue: null
      }];
    case 'registry':
      warnings.push('Registry detection requires structured key/value fields; emitted a best-effort rule for review.');
      return [{
        '@odata.type': '#microsoft.graph.win32LobAppRegistryDetection',
        keyPath: rule || 'HKEY_LOCAL_MACHINE\\SOFTWARE',
        valueName: '',
        check32BitOn64System: false,
        detectionType: 'exists',
        operator: 'notConfigured',
        detectionValue: null
      }];
    default:
      warnings.push(`Unknown detection type "${deployment.detectionType}"; defaulted to script detection.`);
      return [{
        '@odata.type': '#microsoft.graph.win32LobAppPowerShellScriptDetection',
        scriptContent: Buffer.from(rule, 'utf8').toString('base64'),
        enforceSignatureCheck: false,
        runAs32Bit: false
      }];
  }
}

export function buildWin32LobAppPayload(deployment, profile = null) {
  const warnings = [];
  if (!deployment) throw new Error('A deployment is required to build a win32LobApp payload');
  const requirements = deployment.requirements || {};
  const displayName = profile?.appName
    ? `${profile.appVendor ? `${profile.appVendor} ` : ''}${profile.appName}${profile.appVersion ? ` ${profile.appVersion}` : ''}`.trim()
    : deployment.name;
  const fileName = deployment.intunewinFile || `${(deployment.name || 'package').replace(/[^A-Za-z0-9._-]+/g, '-')}.intunewin`;

  if (!deployment.installCommand) warnings.push('Install command is empty.');
  if (!deployment.uninstallCommand) warnings.push('Uninstall command is empty.');
  if (!deployment.intunewinFile) warnings.push('No .intunewin file recorded; content must be uploaded before the app is installable (Phase 2).');

  const payload = {
    '@odata.type': '#microsoft.graph.win32LobApp',
    displayName,
    description: deployment.notes || displayName,
    publisher: profile?.appVendor || 'POSHManager',
    fileName,
    setupFilePath: fileName,
    installCommandLine: deployment.installCommand || '',
    uninstallCommandLine: deployment.uninstallCommand || '',
    applicableArchitectures: applicableArchitectures(requirements.architecture),
    allowAvailableUninstall: true,
    minimumFreeDiskSpaceInMB: Number(requirements.diskSpaceMb) > 0 ? Number(requirements.diskSpaceMb) : null,
    minimumSupportedOperatingSystem: minimumSupportedOperatingSystem(requirements.minOs),
    installExperience: {
      '@odata.type': 'microsoft.graph.win32LobAppInstallExperience',
      runAsAccount: deployment.installBehavior === 'user' ? 'user' : 'system',
      deviceRestartBehavior: RESTART_BEHAVIOR[deployment.restartBehavior] || 'basedOnReturnCode'
    },
    returnCodes: mapReturnCodes(deployment.returnCodes),
    detectionRules: mapDetectionRules(deployment, warnings),
    requirementRules: [],
    msiInformation: null,
    runAs32Bit: Boolean(requirements.runAs32Bit)
  };

  return { payload, warnings };
}

// ---------------------------------------------------------------------------
// Phase 3 — assignments / filters / rings
// ---------------------------------------------------------------------------

const ASSIGNMENT_INTENTS = new Set(['available', 'required', 'uninstall']);

function assignmentTarget(assignment, warnings) {
  const filter = assignment.filterId
    ? {
        deviceAndAppManagementAssignmentFilterId: assignment.filterId,
        deviceAndAppManagementAssignmentFilterType: assignment.filterType && assignment.filterType !== 'none' ? assignment.filterType : 'include'
      }
    : { deviceAndAppManagementAssignmentFilterId: null, deviceAndAppManagementAssignmentFilterType: 'none' };

  if (assignment.groupMode === 'allDevices') {
    return { '@odata.type': '#microsoft.graph.allDevicesAssignmentTarget', ...filter };
  }
  if (assignment.groupMode === 'allUsers') {
    return { '@odata.type': '#microsoft.graph.allLicensedUsersAssignmentTarget', ...filter };
  }
  if (!assignment.groupId) {
    warnings.push(`Assignment "${assignment.ring}" has no Entra group id and was skipped. Resolve the group to its object id first.`);
    return null;
  }
  const isExclusion = assignment.intent === 'exclude';
  return {
    '@odata.type': isExclusion
      ? '#microsoft.graph.exclusionGroupAssignmentTarget'
      : '#microsoft.graph.groupAssignmentTarget',
    groupId: assignment.groupId,
    ...filter
  };
}

// Build the `mobileAppAssignments` array for the /assign action. Exclusions map
// to exclusionGroupAssignmentTarget (Graph still requires a real intent, so we
// exclude from "required"). Group assignments without a resolved object id are
// skipped with a warning rather than producing an invalid request.
export function buildAssignmentsPayload(deployment) {
  const warnings = [];
  const assignments = [];
  for (const assignment of deployment.assignments || []) {
    const target = assignmentTarget(assignment, warnings);
    if (!target) continue;
    const intent = assignment.intent === 'exclude'
      ? 'required'
      : (ASSIGNMENT_INTENTS.has(assignment.intent) ? assignment.intent : 'required');
    assignments.push({
      '@odata.type': '#microsoft.graph.mobileAppAssignment',
      intent,
      target,
      settings: {
        '@odata.type': '#microsoft.graph.win32LobAppAssignmentSettings',
        notifications: 'showAll',
        restartSettings: null,
        installTimeSettings: null,
        deliveryOptimizationPriority: 'notConfigured',
        autoUpdateSettings: assignment.autoUpdate
          ? { '@odata.type': '#microsoft.graph.win32LobAppAutoUpdateSettings', autoUpdateSupersededApps: 'enabled' }
          : null
      }
    });
  }
  if (!assignments.length) warnings.push('No assignable targets were produced; nothing would be assigned.');
  return { assignments, warnings };
}

// ---------------------------------------------------------------------------
// Phase 4 — supersedence & dependency relationships
// ---------------------------------------------------------------------------

// Intune limits a supersedence graph to 11 nodes total (this app + superseded +
// their related apps). We can only see this app's direct edges, so we enforce a
// local ceiling of 10 supersedence targets and flag that the full graph limit is
// tenant-wide. Self-references and duplicates are rejected as cycles.
export const MAX_SUPERSEDENCE_TARGETS = 10;

export function validateRelationships(relationships = [], selfAppId = '') {
  const errors = [];
  const seen = new Set();
  let supersedenceCount = 0;
  for (const rel of relationships) {
    const target = rel.targetAppId;
    if (!target) { errors.push('A relationship is missing targetAppId.'); continue; }
    if (selfAppId && target === selfAppId) errors.push(`An app cannot have a ${rel.kind} relationship with itself (${target}).`);
    const key = `${rel.kind}:${target}`;
    if (seen.has(key)) errors.push(`Duplicate ${rel.kind} relationship to ${target}.`);
    seen.add(key);
    if (rel.kind === 'supersedence') supersedenceCount += 1;
  }
  if (supersedenceCount > MAX_SUPERSEDENCE_TARGETS) {
    errors.push(`Too many supersedence targets (${supersedenceCount}); Intune allows at most ${MAX_SUPERSEDENCE_TARGETS} direct targets (11-node graph limit).`);
  }
  return errors;
}

export function buildRelationshipsPayload(relationships = [], selfAppId = '') {
  const errors = validateRelationships(relationships, selfAppId);
  if (errors.length) {
    const error = new Error(errors.join(' '));
    error.validation = errors;
    throw error;
  }
  const mapped = relationships.map((rel) => {
    if (rel.kind === 'dependency') {
      return {
        '@odata.type': '#microsoft.graph.mobileAppDependency',
        targetId: rel.targetAppId,
        dependencyType: rel.mode === 'autoInstall' ? 'autoInstall' : 'detect'
      };
    }
    return {
      '@odata.type': '#microsoft.graph.mobileAppSupersedence',
      targetId: rel.targetAppId,
      supersedenceType: rel.mode === 'replace' ? 'replace' : 'update'
    };
  });
  return { relationships: mapped };
}
