package ai.decompile.engine.service;

import ai.decompile.docker.service.ContainerService;
import ai.decompile.engine.config.EngineProperties;
import ai.decompile.engine.model.AnalysisResult;
import ai.decompile.engine.model.EnumEntry;
import ai.decompile.engine.model.FunctionResult;
import ai.decompile.engine.model.GlobalVarEntry;
import ai.decompile.engine.model.ImportEntry;
import ai.decompile.engine.model.LabelEntry;
import ai.decompile.engine.model.SegmentEntry;
import ai.decompile.engine.model.StringEntry;
import ai.decompile.engine.model.StructEntry;
import ai.decompile.engine.model.XrefEntry;
import com.github.dockerjava.api.model.Bind;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import lombok.extern.log4j.Log4j2;

@Log4j2
public abstract class AbstractIdaEngineService implements EngineService {
  private static final String KEY_ENTRY_POINT = "\"entry_point\": \"";
  private static final String KEY_ADDRESS = "\",\n      \"address\": \"";
  private static final String KEY_SIZE = "\",\n      \"size\": ";
  private static final String KEY_FLAGS = ",\n      \"flags\": \"";
  private static final String KEY_XREFS = "\",\n      \"xrefs\": \"";
  private static final String KEY_ASSEMBLY = "\",\n      \"assembly\": \"";
  private static final String KEY_LABELS = "\",\n      \"labels\": \"";
  private static final String KEY_ENTRY_END = "\"\n    }";
  private static final String KEY_SEGMENTS_START = "\"segments\": [";
  private static final String KEY_SEGMENT_NAME = "\"name\": \"";
  private static final String KEY_SEGMENT_START = "\", \"start\": \"";
  private static final String KEY_SEGMENT_END = "\", \"end\": \"";
  private static final String KEY_STRINGS_START = "\"strings\": [";
  private static final String KEY_STRING_ADDRESS = "\"address\": \"";
  private static final String KEY_STRING_VALUE = "\", \"value\": \"";
  private static final String KEY_STRING_ENCODING = "\", \"encoding\": \"";
  private static final String KEY_STRING_REFERENCED_BY = ", \"referencedBy\": \"";
  private static final String KEY_STACK_FRAME = "\",\n      \"stackFrame\": \"";
  private static final String KEY_COMMENTS = "\",\n      \"comments\": \"";
  private static final String KEY_SIGNATURE = "\",\n      \"signature\": \"";
  private static final String KEY_CFG_BLOCKS = "\",\n      \"cfgBlocks\": \"";
  private static final String KEY_IMPORTS_START = "\"imports\": [";
  private static final String KEY_IMPORT_DLL = "\"dll\": \"";
  private static final String KEY_IMPORT_NAME = "\", \"name\": \"";
  private static final String KEY_IMPORT_ADDRESS = "\", \"address\": \"";
  private static final String KEY_IMPORT_ORDINAL = "\", \"ordinal\": ";
  private static final String KEY_GLOBAL_VARS_START = "\"globalVars\": [";
  private static final String KEY_GVAR_NAME = "\"name\": \"";
  private static final String KEY_GVAR_ADDRESS = "\", \"address\": \"";
  private static final String KEY_GVAR_SEGMENT = "\", \"segment\": \"";
  private static final String KEY_GVAR_SIZE = "\", \"size\": ";
  private static final String KEY_GVAR_DATA_XREFS = ", \"dataXrefs\": \"";
  private static final String KEY_STRUCTURES_START = "\"structures\": [";
  private static final String KEY_STRUCT_NAME = "\"name\": \"";
  private static final String KEY_STRUCT_SIZE = "\", \"size\": ";
  private static final String KEY_STRUCT_IS_UNION = ", \"isUnion\": ";
  private static final String KEY_STRUCT_MEMBERS = ", \"members\": \"";
  private static final String KEY_ENUMS_START = "\"enums\": [";
  private static final String KEY_ENUM_NAME = "\"name\": \"";
  private static final String KEY_ENUM_WIDTH = "\", \"width\": ";
  private static final String KEY_ENUM_CONSTANTS = ", \"constants\": \"";

  protected final ContainerService containers;
  protected final EngineProperties engineProperties;

  protected AbstractIdaEngineService(
      ContainerService containers, EngineProperties engineProperties) {
    this.containers = containers;
    this.engineProperties = engineProperties;
  }

  protected abstract String getDockerfilePath();

  protected abstract String getConfigKey();

  protected abstract String getTempDirPrefix();

  @Override
  public AnalysisResult analyze(String filePath) {
    return analyze(filePath, List.of());
  }

  @Override
  public AnalysisResult analyze(String filePath, List<String> dependencyPaths) {
    var configKey = getConfigKey();
    var config = engineProperties.getEngineConfig(configKey);
    var absolutePath = Path.of(filePath).toAbsolutePath().normalize();
    var filename = absolutePath.getFileName().toString();

    containers.ensureImage(config.getImage(), new File(getDockerfilePath()));

    var outputDir = createTempDir();
    var dllDir = createDllDir(dependencyPaths);
    var containerId = createContainer(config.getImage(), filename, absolutePath, outputDir, dllDir);

    try {
      var exitCode = containers.waitForExit(containerId, config.getTimeoutSeconds());
      var logs = containers.captureLogs(containerId, 2);

      validateExitCode(configKey, exitCode, logs);
      var outputFile = validateOutputFile(configKey, outputDir, logs);
      return parseOutput(readStringTolerant(outputFile));
    } catch (Exception e) {
      log.error("{} analysis failed for file {}: {}", configKey, filePath, e.getMessage());
      throw new RuntimeException(configKey + " analysis failed: " + e.getMessage(), e);
    } finally {
      containers.stopAndRemove(containerId);
      cleanupTempDir(outputDir);
      cleanupTempDir(dllDir);
    }
  }

  private Path createTempDir() {
    try {
      return Files.createTempDirectory(getTempDirPrefix());
    } catch (IOException e) {
      throw new RuntimeException("Failed to create temporary output directory", e);
    }
  }

  private Path createDllDir(List<String> dependencyPaths) {
    if (Objects.isNull(dependencyPaths) || dependencyPaths.isEmpty()) {
      return null;
    }

    try {
      var dllDir = Files.createTempDirectory(getTempDirPrefix() + "dlls-");
      for (var depPath : dependencyPaths) {
        var source = Path.of(depPath);
        var target = dllDir.resolve(source.getFileName());
        Files.copy(source, target);
      }

      return dllDir;
    } catch (IOException e) {
      throw new RuntimeException("Failed to create DLL temp directory", e);
    }
  }

  private String createContainer(
      String image, String filename, Path absolutePath, Path outputDir, Path dllDir) {
    var inputBind = Bind.parse(absolutePath.getParent() + ":/input:ro");
    var outputBind = Bind.parse(outputDir.toAbsolutePath() + ":/output:rw");

    Bind[] binds;
    if (Objects.nonNull(dllDir)) {
      var dllBind = Bind.parse(dllDir.toAbsolutePath() + ":/dlls:ro");
      binds = new Bind[] {inputBind, outputBind, dllBind};
    } else {
      binds = new Bind[] {inputBind, outputBind};
    }

    var containerId = containers.createContainer(image, new String[] {filename}, binds);
    containers.startContainer(containerId);
    return containerId;
  }

  private static void validateExitCode(String configKey, int exitCode, String logs) {
    if (exitCode != 0) {
      log.error("{} container exited with code {}: {}", configKey, exitCode, logs);
      throw new RuntimeException(
          configKey + " analysis failed with exit code " + exitCode + ". Output: " + logs);
    }
  }

  private static Path validateOutputFile(String configKey, Path outputDir, String logs) {
    var outputFile = outputDir.resolve("output.json");
    if (!Files.exists(outputFile)) {
      throw new RuntimeException(
          configKey + " analysis did not produce output.json. Container logs: " + logs);
    }

    return outputFile;
  }

  private static void cleanupTempDir(Path dir) {
    if (Objects.isNull(dir)) {
      return;
    }

    try (var walk = Files.walk(dir)) {
      walk.sorted(Comparator.reverseOrder()).forEach(AbstractIdaEngineService::deleteSilently);
    } catch (IOException ignored) {
    }
  }

  private static void deleteSilently(Path path) {
    try {
      Files.deleteIfExists(path);
    } catch (IOException ignored) {
    }
  }

  private static String readStringTolerant(Path path) throws IOException {
    var decoder =
        StandardCharsets.UTF_8
            .newDecoder()
            .onMalformedInput(CodingErrorAction.REPLACE)
            .onUnmappableCharacter(CodingErrorAction.REPLACE);
    return decoder.decode(ByteBuffer.wrap(Files.readAllBytes(path))).toString();
  }

  private static AnalysisResult parseOutput(String raw) {
    var text = raw.replace("\r\n", "\n");

    var entryPoint = parseEntryPoint(text);
    var segments = parseSegments(text);
    var strings = parseStrings(text);
    var imports = parseImports(text);
    var globalVars = parseGlobalVars(text);
    var structures = parseStructures(text);
    var enums = parseEnums(text);

    var functions = new ArrayList<FunctionResult>();

    var sections = text.split("\"function\"\\s*:\\s*\"");
    for (int i = 1; i < sections.length; i++) {
      var func = parseFunctionEntry(sections[i]);
      if (Objects.nonNull(func)) {
        functions.add(func);
      }
    }

    if (functions.isEmpty()) {
      log.warn(
          "IDA produced no parsable function entries. Raw output (first 500 chars): {}",
          raw.length() > 500 ? raw.substring(0, 500) : raw);
    }

    return new AnalysisResult(
        entryPoint, segments, functions, strings, imports, globalVars, structures, enums);
  }

  private static String parseEntryPoint(String text) {
    var entryRemainder = after(text, KEY_ENTRY_POINT);
    if (Objects.isNull(entryRemainder)) {
      return "unknown";
    }

    var entryVal = extractField(entryRemainder, "\"");
    if (Objects.isNull(entryVal)) {
      return "unknown";
    }

    return entryVal;
  }

  private static FunctionResult parseFunctionEntry(String section) {
    var name = extractField(section, KEY_ADDRESS);
    if (Objects.isNull(name)) {
      return null;
    }

    var remainder = after(section, KEY_ADDRESS);
    if (Objects.isNull(remainder)) {
      return null;
    }
    var address = extractField(remainder, KEY_SIZE);
    if (Objects.isNull(address)) {
      return null;
    }

    remainder = after(remainder, KEY_SIZE);
    if (Objects.isNull(remainder)) {
      return null;
    }
    var sizeStr = extractField(remainder, KEY_FLAGS);
    if (Objects.isNull(sizeStr)) {
      return null;
    }
    var size = parseSize(sizeStr);

    remainder = after(remainder, KEY_FLAGS);
    if (Objects.isNull(remainder)) {
      return null;
    }
    var flags = extractField(remainder, KEY_XREFS);
    if (Objects.isNull(flags)) {
      return null;
    }

    remainder = after(remainder, KEY_XREFS);
    if (Objects.isNull(remainder)) {
      return null;
    }
    var xrefsRaw = extractField(remainder, KEY_ASSEMBLY);
    if (Objects.isNull(xrefsRaw)) {
      return null;
    }

    remainder = after(remainder, KEY_ASSEMBLY);
    if (Objects.isNull(remainder)) {
      return null;
    }
    var assembly = extractField(remainder, KEY_LABELS);
    if (Objects.isNull(assembly)) {
      return null;
    }

    remainder = after(remainder, KEY_LABELS);
    if (Objects.isNull(remainder)) {
      return null;
    }

    var labelsRaw = extractField(remainder, KEY_STACK_FRAME);
    if (Objects.isNull(labelsRaw)) {
      return null;
    }

    var sfRemainder = after(remainder, KEY_STACK_FRAME);
    if (Objects.isNull(sfRemainder)) {
      return null;
    }
    var stackFrame = extractField(sfRemainder, KEY_COMMENTS);
    if (Objects.isNull(stackFrame)) {
      return null;
    }

    var cmtRemainder = after(sfRemainder, KEY_COMMENTS);
    if (Objects.isNull(cmtRemainder)) {
      return null;
    }

    var comments = extractField(cmtRemainder, KEY_CFG_BLOCKS);
    var cfgBlocks = "[]";
    var signature = "";
    if (Objects.isNull(comments)) {
      comments = extractField(cmtRemainder, KEY_SIGNATURE);
      if (Objects.isNull(comments)) {
        comments = extractField(cmtRemainder, KEY_ENTRY_END);
        if (Objects.isNull(comments)) {
          return null;
        }
      } else {
        var sigRemainder = after(cmtRemainder, KEY_SIGNATURE);
        if (Objects.nonNull(sigRemainder)) {
          var sig = extractField(sigRemainder, KEY_ENTRY_END);
          if (Objects.nonNull(sig)) {
            signature = sig;
          }
        }
      }
    } else {
      var cfgRemainder = after(cmtRemainder, KEY_CFG_BLOCKS);
      if (Objects.nonNull(cfgRemainder)) {
        var cfg = extractField(cfgRemainder, KEY_SIGNATURE);
        if (Objects.nonNull(cfg)) {
          cfgBlocks = cfg;
        }
        var sigRemainder = after(cfgRemainder, KEY_SIGNATURE);
        if (Objects.nonNull(sigRemainder)) {
          var sig = extractField(sigRemainder, KEY_ENTRY_END);
          if (Objects.nonNull(sig)) {
            signature = sig;
          }
        }
      }
    }

    var xrefs = parseXrefs(xrefsRaw.trim());
    var labels = parseLabels(labelsRaw.trim());
    return new FunctionResult(
        name,
        address,
        size,
        flags.endsWith("|") ? flags.substring(0, flags.length() - 1) : flags,
        assembly,
        null,
        signature,
        xrefs,
        labels,
        stackFrame,
        comments,
        cfgBlocks);
  }

  private static int parseSize(String sizeStr) {
    try {
      return Integer.parseInt(sizeStr.trim());
    } catch (NumberFormatException e) {
      return 0;
    }
  }

  private static String extractField(String text, String endDelim) {
    var end = text.indexOf(endDelim);
    return end < 0 ? null : text.substring(0, end);
  }

  private static String after(String text, String delim) {
    var idx = text.indexOf(delim);
    return idx < 0 ? null : text.substring(idx + delim.length());
  }

  private static String stripBraces(String part) {
    var trimmed = part.trim();
    if (trimmed.startsWith("{")) {
      trimmed = trimmed.substring(1);
    }

    if (trimmed.endsWith("}")) {
      trimmed = trimmed.substring(0, trimmed.length() - 1);
    }

    return trimmed.trim();
  }

  private static List<String> splitEntries(String block) {
    var parts = new ArrayList<String>();
    int depth = 0;
    boolean inString = false;
    int start = 0;
    for (int i = 0; i < block.length(); i++) {
      var c = block.charAt(i);
      if (c == '\\' && inString && i + 1 < block.length()) {
        i++;
        continue;
      }

      if (c == '"') {
        inString = !inString;
        continue;
      }

      if (inString) {
        continue;
      }

      if (c == '{') {
        depth++;
      } else if (c == '}') {
        depth--;
      }

      if (c == ',' && depth == 0 && i + 1 < block.length()) {
        var nextIdx = i + 1;
        while (nextIdx < block.length() && Character.isWhitespace(block.charAt(nextIdx))) {
          nextIdx++;
        }

        if (nextIdx < block.length() && block.charAt(nextIdx) == '{') {
          parts.add(block.substring(start, i));
          start = i + 1;
        }
      }
    }

    if (start < block.length()) {
      parts.add(block.substring(start));
    }

    return parts;
  }

  private static List<SegmentEntry> parseSegments(String text) {
    var segments = new ArrayList<SegmentEntry>();
    var remainder = after(text, KEY_SEGMENTS_START);
    if (Objects.isNull(remainder)) {
      return segments;
    }

    var segmentsBlock = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(segmentsBlock)) {
      return segments;
    }

    var parts = splitEntries(segmentsBlock);
    for (var part : parts) {
      var segment = parseSegmentEntry(part);
      if (Objects.nonNull(segment)) {
        segments.add(segment);
      }
    }

    return segments;
  }

  private static SegmentEntry parseSegmentEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var nameRemainder = after(clean, KEY_SEGMENT_NAME);
    if (Objects.isNull(nameRemainder)) {
      return null;
    }

    var name = extractField(nameRemainder, KEY_SEGMENT_START);
    if (Objects.isNull(name)) {
      return null;
    }

    var startRemainder = after(nameRemainder, KEY_SEGMENT_START);
    if (Objects.isNull(startRemainder)) {
      return null;
    }

    var startAddress = extractField(startRemainder, KEY_SEGMENT_END);
    if (Objects.isNull(startAddress)) {
      return null;
    }

    var endRemainder = after(startRemainder, KEY_SEGMENT_END);
    if (Objects.isNull(endRemainder)) {
      return null;
    }

    var endAddress = extractField(endRemainder, "\"");
    if (Objects.isNull(endAddress)) {
      return null;
    }

    return new SegmentEntry(name, startAddress, endAddress);
  }

  private static List<StringEntry> parseStrings(String text) {
    var strings = new ArrayList<StringEntry>();
    var remainder = after(text, KEY_STRINGS_START);
    if (Objects.isNull(remainder)) {
      return strings;
    }

    var stringsBlock = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(stringsBlock)) {
      return strings;
    }

    var parts = splitEntries(stringsBlock);
    for (var part : parts) {
      var string = parseStringEntry(part);
      if (Objects.nonNull(string)) {
        strings.add(string);
      }
    }

    return strings;
  }

  private static StringEntry parseStringEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var addressRemainder = after(clean, KEY_STRING_ADDRESS);
    if (Objects.isNull(addressRemainder)) {
      return null;
    }

    var address = extractField(addressRemainder, KEY_STRING_VALUE);
    if (Objects.isNull(address)) {
      return null;
    }

    var valueRemainder = after(addressRemainder, KEY_STRING_VALUE);
    if (Objects.isNull(valueRemainder)) {
      return null;
    }

    var value = extractField(valueRemainder, KEY_STRING_ENCODING);
    if (Objects.isNull(value)) {
      return null;
    }

    var encodingRemainder = after(valueRemainder, KEY_STRING_ENCODING);
    if (Objects.isNull(encodingRemainder)) {
      return null;
    }

    var encoding = extractField(encodingRemainder, "\"");
    if (Objects.isNull(encoding)) {
      return null;
    }

    var afterEncoding = after(encodingRemainder, "\"");
    var referencedBy = "";
    if (Objects.nonNull(afterEncoding)) {
      var refRemainder = after(afterEncoding, KEY_STRING_REFERENCED_BY);
      if (Objects.nonNull(refRemainder)) {
        var ref = extractField(refRemainder, "\"");
        if (Objects.nonNull(ref)) {
          referencedBy = ref;
        }
      }
    }

    return new StringEntry(address, value, encoding, referencedBy);
  }

  private static List<ImportEntry> parseImports(String text) {
    var imports = new ArrayList<ImportEntry>();
    var remainder = after(text, KEY_IMPORTS_START);
    if (Objects.isNull(remainder)) {
      return imports;
    }

    var importsBlock = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(importsBlock)) {
      return imports;
    }

    var parts = splitEntries(importsBlock);
    for (var part : parts) {
      var imp = parseImportEntry(part);
      if (Objects.nonNull(imp)) {
        imports.add(imp);
      }
    }

    return imports;
  }

  private static ImportEntry parseImportEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var dllRemainder = after(clean, KEY_IMPORT_DLL);
    if (Objects.isNull(dllRemainder)) {
      return null;
    }

    var dll = extractField(dllRemainder, KEY_IMPORT_NAME);
    if (Objects.isNull(dll)) {
      return null;
    }

    var nameRemainder = after(dllRemainder, KEY_IMPORT_NAME);
    if (Objects.isNull(nameRemainder)) {
      return null;
    }

    var name = extractField(nameRemainder, KEY_IMPORT_ADDRESS);
    if (Objects.isNull(name)) {
      return null;
    }

    var addrRemainder = after(nameRemainder, KEY_IMPORT_ADDRESS);
    if (Objects.isNull(addrRemainder)) {
      return null;
    }

    var address = extractField(addrRemainder, KEY_IMPORT_ORDINAL);
    if (Objects.isNull(address)) {
      return null;
    }

    var ordinalRemainder = after(addrRemainder, KEY_IMPORT_ORDINAL);
    if (Objects.isNull(ordinalRemainder)) {
      return null;
    }

    var ordinalStr = extractField(ordinalRemainder, ",");
    if (Objects.isNull(ordinalStr)) {
      ordinalStr = extractField(ordinalRemainder, "\"");
    }
    if (Objects.isNull(ordinalStr)) {
      ordinalStr = ordinalRemainder;
    }
    ordinalStr = ordinalStr.trim();
    if (ordinalStr.isEmpty()) {
      ordinalStr = "0";
    }
    int ordinal;
    try {
      ordinal = Integer.parseInt(ordinalStr);
    } catch (NumberFormatException e) {
      ordinal = 0;
    }

    return new ImportEntry(dll, name, address, ordinal);
  }

  private static List<GlobalVarEntry> parseGlobalVars(String text) {
    var globalVars = new ArrayList<GlobalVarEntry>();
    var remainder = after(text, KEY_GLOBAL_VARS_START);
    if (Objects.isNull(remainder)) {
      return globalVars;
    }

    var gvBlock = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(gvBlock)) {
      return globalVars;
    }

    var parts = splitEntries(gvBlock);
    for (var part : parts) {
      var gv = parseGlobalVarEntry(part);
      if (Objects.nonNull(gv)) {
        globalVars.add(gv);
      }
    }

    return globalVars;
  }

  private static GlobalVarEntry parseGlobalVarEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var nameRemainder = after(clean, KEY_GVAR_NAME);
    if (Objects.isNull(nameRemainder)) {
      return null;
    }

    var name = extractField(nameRemainder, KEY_GVAR_ADDRESS);
    if (Objects.isNull(name)) {
      return null;
    }

    var addrRemainder = after(nameRemainder, KEY_GVAR_ADDRESS);
    if (Objects.isNull(addrRemainder)) {
      return null;
    }

    var address = extractField(addrRemainder, KEY_GVAR_SEGMENT);
    if (Objects.isNull(address)) {
      return null;
    }

    var segRemainder = after(addrRemainder, KEY_GVAR_SEGMENT);
    if (Objects.isNull(segRemainder)) {
      return null;
    }

    var segment = extractField(segRemainder, KEY_GVAR_SIZE);
    if (Objects.isNull(segment)) {
      return null;
    }

    var sizeRemainder = after(segRemainder, KEY_GVAR_SIZE);
    if (Objects.isNull(sizeRemainder)) {
      return null;
    }

    var sizeStr = extractField(sizeRemainder, KEY_GVAR_DATA_XREFS);
    if (Objects.isNull(sizeStr)) {
      return null;
    }
    int size;
    try {
      size = Integer.parseInt(sizeStr.trim());
    } catch (NumberFormatException e) {
      size = 0;
    }

    var xrefRemainder = after(sizeRemainder, KEY_GVAR_DATA_XREFS);
    if (Objects.isNull(xrefRemainder)) {
      return null;
    }

    var dataXrefs =
        xrefRemainder.endsWith("\"")
            ? xrefRemainder.substring(0, xrefRemainder.length() - 1)
            : xrefRemainder;

    return new GlobalVarEntry(name, address, segment, size, dataXrefs);
  }

  private static List<StructEntry> parseStructures(String text) {
    var structures = new ArrayList<StructEntry>();
    var remainder = after(text, KEY_STRUCTURES_START);
    if (Objects.isNull(remainder)) {
      return structures;
    }

    var block = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(block)) {
      return structures;
    }

    var parts = splitEntries(block);
    for (var part : parts) {
      var s = parseStructEntry(part);
      if (Objects.nonNull(s)) {
        structures.add(s);
      }
    }

    return structures;
  }

  private static StructEntry parseStructEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var nameRemainder = after(clean, KEY_STRUCT_NAME);
    if (Objects.isNull(nameRemainder)) {
      return null;
    }
    var name = extractField(nameRemainder, KEY_STRUCT_SIZE);
    if (Objects.isNull(name)) {
      return null;
    }

    var sizeRemainder = after(nameRemainder, KEY_STRUCT_SIZE);
    if (Objects.isNull(sizeRemainder)) {
      return null;
    }
    var sizeStr = extractField(sizeRemainder, KEY_STRUCT_IS_UNION);
    if (Objects.isNull(sizeStr)) {
      return null;
    }
    int size;
    try {
      size = Integer.parseInt(sizeStr.trim());
    } catch (NumberFormatException e) {
      size = 0;
    }

    var unionRemainder = after(sizeRemainder, KEY_STRUCT_IS_UNION);
    if (Objects.isNull(unionRemainder)) {
      return null;
    }
    var isUnionStr = extractField(unionRemainder, KEY_STRUCT_MEMBERS);
    if (Objects.isNull(isUnionStr)) {
      return null;
    }
    boolean isUnion = "true".equals(isUnionStr.trim());

    var memRemainder = after(unionRemainder, KEY_STRUCT_MEMBERS);
    if (Objects.isNull(memRemainder)) {
      return null;
    }
    var members =
        memRemainder.endsWith("\"")
            ? memRemainder.substring(0, memRemainder.length() - 1)
            : memRemainder;

    return new StructEntry(name, size, isUnion, members);
  }

  private static List<EnumEntry> parseEnums(String text) {
    var enums = new ArrayList<EnumEntry>();
    var remainder = after(text, KEY_ENUMS_START);
    if (Objects.isNull(remainder)) {
      return enums;
    }

    var block = extractField(remainder, "\n  ],\n");
    if (Objects.isNull(block)) {
      return enums;
    }

    var parts = splitEntries(block);
    for (var part : parts) {
      var e = parseEnumEntry(part);
      if (Objects.nonNull(e)) {
        enums.add(e);
      }
    }

    return enums;
  }

  private static EnumEntry parseEnumEntry(String part) {
    var clean = stripBraces(part);
    if (clean.isBlank()) {
      return null;
    }

    var nameRemainder = after(clean, KEY_ENUM_NAME);
    if (Objects.isNull(nameRemainder)) {
      return null;
    }
    var name = extractField(nameRemainder, KEY_ENUM_WIDTH);
    if (Objects.isNull(name)) {
      return null;
    }

    var widthRemainder = after(nameRemainder, KEY_ENUM_WIDTH);
    if (Objects.isNull(widthRemainder)) {
      return null;
    }
    var widthStr = extractField(widthRemainder, KEY_ENUM_CONSTANTS);
    if (Objects.isNull(widthStr)) {
      return null;
    }
    int width;
    try {
      width = Integer.parseInt(widthStr.trim());
    } catch (NumberFormatException e) {
      width = 0;
    }

    var constRemainder = after(widthRemainder, KEY_ENUM_CONSTANTS);
    if (Objects.isNull(constRemainder)) {
      return null;
    }
    var constants =
        constRemainder.endsWith("\"")
            ? constRemainder.substring(0, constRemainder.length() - 1)
            : constRemainder;

    return new EnumEntry(name, width, constants);
  }

  private static List<XrefEntry> parseXrefs(String raw) {
    if (Objects.isNull(raw) || raw.isBlank()) {
      return List.of();
    }

    return Arrays.stream(raw.trim().split("\\s+"))
        .map(AbstractIdaEngineService::parseXrefEntry)
        .toList();
  }

  private static XrefEntry parseXrefEntry(String token) {
    var colon = token.lastIndexOf(':');
    if (colon < 0) {
      return new XrefEntry(token, "CALL");
    }

    return new XrefEntry(token.substring(0, colon), token.substring(colon + 1));
  }

  private static List<LabelEntry> parseLabels(String raw) {
    if (Objects.isNull(raw) || raw.isBlank()) {
      return List.of();
    }

    return Arrays.stream(raw.trim().split("\\s+"))
        .distinct()
        .map(AbstractIdaEngineService::parseLabelEntry)
        .toList();
  }

  private static LabelEntry parseLabelEntry(String token) {
    var lastColon = token.lastIndexOf(':');
    if (lastColon < 0) {
      return new LabelEntry(token, "", "CALL");
    }

    var type = token.substring(lastColon + 1);
    var nameAddr = token.substring(0, lastColon);
    var addrColon = nameAddr.lastIndexOf(':');
    if (addrColon < 0) {
      return new LabelEntry(nameAddr, "", type);
    }

    return new LabelEntry(
        nameAddr.substring(0, addrColon), nameAddr.substring(addrColon + 1), type);
  }
}
