function cleanManifest(manifestText) {
  return manifestText
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line && !line.startsWith("#"));
}

function normalizeRel(path) {
  const parts = [];
  for (const part of path.split("/")) {
    if (!part || part === ".") {
      continue;
    }
    if (part === "..") {
      parts.pop();
      continue;
    }
    parts.push(part);
  }
  return parts.join("/");
}

function dirname(path) {
  const clean = normalizeRel(path);
  const index = clean.lastIndexOf("/");
  return index === -1 ? "" : clean.slice(0, index);
}

function basename(path) {
  const clean = normalizeRel(path);
  const index = clean.lastIndexOf("/");
  return index === -1 ? clean : clean.slice(index + 1);
}

function stripIniComment(line) {
  const hash = line.indexOf("#");
  const semicolon = line.indexOf(";");
  const indexes = [hash, semicolon].filter((index) => index >= 0);
  if (indexes.length === 0) {
    return line;
  }
  return line.slice(0, Math.min(...indexes));
}

function parseIni(iniText) {
  const values = new Map();
  let section = "";
  for (const rawLine of iniText.split("\n")) {
    const sectionMatch = rawLine.match(/^\s*\[([^\]]+)\]/);
    if (sectionMatch) {
      section = sectionMatch[1].trim().toUpperCase();
      continue;
    }

    const line = stripIniComment(rawLine);
    const equals = line.indexOf("=");
    if (equals === -1 || !section) {
      continue;
    }
    const key = line.slice(0, equals).trim().toUpperCase();
    const value = line.slice(equals + 1).trim();
    if (!key || !value) {
      continue;
    }
    const mapKey = `${section}.${key}`;
    const existing = values.get(mapKey) ?? [];
    existing.push(value);
    values.set(mapKey, existing);
  }
  return values;
}

function firstIniValue(values, section, key) {
  return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`)?.[0] ?? null;
}

function allIniValues(values, section, key) {
  return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`) ?? [];
}

function splitSearchPath(value) {
  return value.split(":").map((entry) => entry.trim()).filter(Boolean);
}

function sourceRelFor(machineRel, path) {
  return normalizeRel(`configs/sim/${machineRel}/${path}`);
}

function targetPathFor(wasmDir, targetRel) {
  return `${wasmDir}/${normalizeRel(targetRel)}`;
}

function isUserMCodePath(path) {
  return /^M1\d\d$/i.test(basename(path));
}

function isSubroutinePath(path) {
  return path.toLowerCase().endsWith(".ngc");
}

function findUpwardByBasename(manifestSet, sourceDir, fileName, searchRootRel) {
  let current = sourceDir;
  while (!searchRootRel || current === searchRootRel || current.startsWith(`${searchRootRel}/`)) {
    const candidate = normalizeRel(`${current}/${fileName}`);
    if (manifestSet.has(candidate)) {
      return candidate;
    }
    if (!current || current === searchRootRel) {
      break;
    }
    current = dirname(current);
  }
  return null;
}

function sourceForReference(manifestSet, sourceDir, reference, searchRootRel) {
  if (!reference || reference.startsWith("/")) {
    return null;
  }
  const sourceRel = normalizeRel(`${sourceDir}/${reference}`);
  if (manifestSet.has(sourceRel)) {
    return sourceRel;
  }
  return findUpwardByBasename(manifestSet, sourceDir, basename(reference), searchRootRel);
}

function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
  if (!sourceRel || !manifestSet.has(sourceRel)) {
    if (options.required) {
      throw new Error(`missing vendored sim-config file: ${sourceRel}`);
    }
    return;
  }

  const existing = plan.get(sourceRel);
  const executable = options.executable || isUserMCodePath(sourceRel);
  if (existing) {
    existing.executable ||= executable;
    return;
  }

  plan.set(sourceRel, {
    sourceRel,
    wasmPath: targetPathFor(options.wasmDir, targetRel),
    path: targetPathFor(options.wasmDir, targetRel),
    executable,
  });
}

function addDirectoryFiles(plan, manifestEntries, manifestSet, sourceDir, targetDir, predicate, options) {
  const prefix = sourceDir ? `${sourceDir}/` : "";
  for (const sourceRel of manifestEntries) {
    if (!sourceRel.startsWith(prefix)) {
      continue;
    }
    const childRel = sourceRel.slice(prefix.length);
    if (childRel.includes("/") || !predicate(sourceRel)) {
      continue;
    }
    addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
  }
}

function remapNgcNames(iniValues) {
  const names = [];
  for (const remap of allIniValues(iniValues, "RS274NGC", "REMAP")) {
    const match = remap.match(/(?:^|\s)ngc=([^\s]+)/i);
    if (match) {
      names.push(`${match[1]}.ngc`);
    }
  }
  return names;
}

function sectionValues(iniValues, section) {
  const prefix = `${section.toUpperCase()}.`;
  const values = [];
  for (const [key, entries] of iniValues.entries()) {
    if (!key.startsWith(prefix)) {
      continue;
    }
    for (const value of entries) {
      values.push({ key: key.slice(prefix.length), value });
    }
  }
  return values;
}

function valuesMatching(iniValues, section, keys) {
  const wanted = new Set(keys.map((key) => key.toUpperCase()));
  return sectionValues(iniValues, section)
    .filter((entry) => wanted.has(entry.key))
    .map((entry) => entry.value);
}

function looksLikePythonReference(value) {
  return /(^|\s|=|:)["']?[^"'\s]*\.py(["'\s]|$)/i.test(value);
}

function userMCodesInText(text) {
  const codes = new Set();
  for (const match of text.matchAll(/(?<![A-Za-z0-9_])M\s*(1\d\d)(?![0-9])/gi)) {
    codes.add(`M${match[1]}`);
  }
  return [...codes].sort();
}

function vendoredUserMCodes({
  manifestEntries,
  sourceDir,
  normalizedSearchRoot,
  userMPathValues,
}) {
  const manifestSet = new Set(manifestEntries);
  const executableCodes = new Set();
  for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
    if (dirEntry.startsWith("/")) {
      continue;
    }
    const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
    const prefix = sourceUserMDir ? `${sourceUserMDir}/` : "";
    for (const sourceRel of manifestEntries) {
      if (
        sourceRel.startsWith(prefix) &&
        !sourceRel.slice(prefix.length).includes("/") &&
        isUserMCodePath(sourceRel)
      ) {
        executableCodes.add(basename(sourceRel).toUpperCase());
      }
    }
  }

  for (const dirEntry of userMPathValues.flatMap(splitSearchPath)) {
    if (dirEntry.startsWith("/")) {
      continue;
    }
    const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
    for (let code = 100; code <= 199; code += 1) {
      const candidate = findUpwardByBasename(
        manifestSet,
        sourceUserMDir,
        `M${code}`,
        normalizedSearchRoot,
      );
      if (candidate) {
        executableCodes.add(`M${code}`);
      }
    }
  }

  return [...executableCodes].sort();
}

export function analyzeIniRuntimeBoundaries({
  manifestText = "",
  sourceRootRel,
  sourceSearchRootRel = sourceRootRel,
  iniFile = "",
  iniText,
  executionTexts = [],
}) {
  const manifestEntries = cleanManifest(manifestText);
  const iniValues = parseIni(iniText);
  const normalizedSourceRoot = normalizeRel(sourceRootRel);
  const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
  const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
  const userMPathValues = allIniValues(iniValues, "RS274NGC", "USER_M_PATH");
  const halValues = valuesMatching(iniValues, "HAL", [
    "HALFILE",
    "HALCMD",
    "POSTGUI_HALFILE",
    "HALUI",
  ]);
  const displayValues = valuesMatching(iniValues, "DISPLAY", [
    "DISPLAY",
    "PYVCP",
    "GLADEVCP",
    "EMBED_TAB_COMMAND",
  ]);
  const halUiMdiCommands = allIniValues(iniValues, "HALUI", "MDI_COMMAND");
  const dbProgram = firstIniValue(iniValues, "EMCIO", "DB_PROGRAM");
  const remapValues = allIniValues(iniValues, "RS274NGC", "REMAP");
  const pythonRemapReferences = [
    ...sectionValues(iniValues, "PYTHON").map((entry) => entry.value),
    ...remapValues.filter((value) => /(?:^|\s)python=/i.test(value)),
  ];
  const pythonUiReferences = [
    ...displayValues.filter(looksLikePythonReference),
    ...(dbProgram && looksLikePythonReference(dbProgram) ? [dbProgram] : []),
  ];
  const pythonReferences = [...pythonRemapReferences, ...pythonUiReferences];
  const vendoredUserMCodeList = vendoredUserMCodes({
    manifestEntries,
    sourceDir,
    normalizedSearchRoot,
    userMPathValues,
  });
  const vendoredUserMCodeSet = new Set(vendoredUserMCodeList);
  const hasUserMPath = userMPathValues.length > 0;
  const executionUserMCodes = [...new Set(executionTexts.flatMap(userMCodesInText))].sort();
  const unstagedExecutionUserMCodes = executionUserMCodes
    .filter((code) => !vendoredUserMCodeSet.has(code));
  const hasExternalUserMUse = hasUserMPath && unstagedExecutionUserMCodes.length > 0;

  const dependencies = [];
  if (dbProgram) {
    dependencies.push("tool_database_process");
  }
  if (halValues.length > 0) {
    dependencies.push("hal_process");
  }
  if (displayValues.some((value) => !/^axis$/i.test(value))) {
    dependencies.push("ui_process");
  }
  if (halUiMdiCommands.length > 0) {
    dependencies.push("halui_mdi_process");
  }
  if (pythonReferences.length > 0) {
    dependencies.push("python_runtime");
  }
  if (hasExternalUserMUse) {
    dependencies.push("external_user_m_process");
  }

  let recommendedBlockedKind = "-";
  if (dbProgram) {
    recommendedBlockedKind = "L4-TOOL-DB";
  } else if (hasExternalUserMUse) {
    recommendedBlockedKind = "L4-USER-M-PROCESS";
  } else if (pythonRemapReferences.length > 0) {
    recommendedBlockedKind = "L4-PYTHON-REMAP";
  }

  return {
    dependencies: [...new Set(dependencies)].sort(),
    recommendedBlockedKind,
    toolDatabaseProgram: dbProgram ?? "",
    halRuntime: {
      values: halValues,
      requiresProcess: halValues.length > 0,
    },
    uiRuntime: {
      values: displayValues,
      requiresProcess: displayValues.some((value) => !/^axis$/i.test(value)),
    },
    haluiRuntime: {
      mdiCommands: halUiMdiCommands,
      requiresProcess: halUiMdiCommands.length > 0,
    },
    userMRuntime: {
      paths: userMPathValues,
      vendoredExecutableCount: vendoredUserMCodeList.length,
      executionCodes: executionUserMCodes,
      unstagedExecutionCodes: unstagedExecutionUserMCodes,
      requiresExternalProcess: hasExternalUserMUse,
    },
    pythonRuntime: {
      references: pythonReferences,
      remapReferences: pythonRemapReferences,
      uiReferences: pythonUiReferences,
      requiresProcess: pythonReferences.length > 0,
    },
  };
}

export function planSimConfigStaging({
  manifestText,
  machineRel,
  iniFile,
  iniText,
  programFile = null,
  wasmDir = `/work/sim/${machineRel}`,
}) {
  return planIniFileContextStaging({
    manifestText,
    sourceRootRel: `configs/sim/${machineRel}`,
    sourceSearchRootRel: "configs/sim",
    iniFile,
    iniText,
    programFile,
    wasmDir,
  });
}

export function planIniFileContextStaging({
  manifestText,
  sourceRootRel,
  iniFile,
  iniText,
  programFile = null,
  wasmDir,
  sourceSearchRootRel = sourceRootRel,
}) {
  const manifestEntries = cleanManifest(manifestText);
  const manifestSet = new Set(manifestEntries);
  const iniValues = parseIni(iniText);
  const plan = new Map();
  const normalizedSourceRoot = normalizeRel(sourceRootRel);
  const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
  const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
  const iniSourceRel = normalizeRel(`${normalizedSourceRoot}/${iniFile}`);
  const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");

  addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
  if (programReference) {
    const programSourceRel = sourceForReference(
      manifestSet,
      sourceDir,
      programReference,
      normalizedSearchRoot,
    );
    addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
      wasmDir,
      required: true,
    });
  }

  for (const [section, key] of [
    ["EMCIO", "TOOL_TABLE"],
    ["RS274NGC", "PARAMETER_FILE"],
  ]) {
    const reference = firstIniValue(iniValues, section, key);
    if (!reference) {
      continue;
    }
    const sourceRel = sourceForReference(manifestSet, sourceDir, reference, normalizedSearchRoot);
    addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
  }

  const subroutineDirs = allIniValues(iniValues, "RS274NGC", "SUBROUTINE_PATH")
    .flatMap(splitSearchPath);
  for (const dirEntry of subroutineDirs) {
    if (dirEntry.startsWith("/")) {
      continue;
    }
    const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
    const targetSubdir = normalizeRel(dirEntry);
    addDirectoryFiles(
      plan,
      manifestEntries,
      manifestSet,
      sourceSubdir,
      targetSubdir,
      isSubroutinePath,
      { wasmDir },
    );
  }

  for (const dirEntry of allIniValues(iniValues, "RS274NGC", "USER_M_PATH").flatMap(splitSearchPath)) {
    if (dirEntry.startsWith("/")) {
      continue;
    }
    const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
    const targetUserMDir = normalizeRel(dirEntry);
    addDirectoryFiles(
      plan,
      manifestEntries,
      manifestSet,
      sourceUserMDir,
      targetUserMDir,
      isUserMCodePath,
      { wasmDir, executable: true },
    );
  }

  for (const remapName of remapNgcNames(iniValues)) {
    for (const dirEntry of subroutineDirs) {
      const sourceRel = sourceForReference(
        manifestSet,
        normalizeRel(`${sourceDir}/${dirEntry}`),
        remapName,
        normalizedSearchRoot,
      );
      addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
        wasmDir,
      });
    }
  }

  return {
    wasmDir,
    iniPath: targetPathFor(wasmDir, iniFile),
    programPath: programReference ? targetPathFor(wasmDir, programReference) : null,
    files: [...plan.values()],
  };
}
