public sealed class PeParser : IPeParser
{
    private readonly DosHeaderParser _dosHeaderParser;
    private readonly NtHeadersParser _ntHeadersParser;
    private readonly SectionHeaderParser _sectionHeaderParser;
    private readonly ImportDirectoryParser _importDirectoryParser;
    private readonly ExportDirectoryParser _exportDirectoryParser;
    private readonly ResourceDirectoryParser _resourceDirectoryParser;
    private readonly RelocationDirectoryParser _relocationDirectoryParser;
    private readonly DebugDirectoryParser _debugDirectoryParser;
    private readonly TlsDirectoryParser _tlsDirectoryParser;
    private readonly LoadConfigDirectoryParser _loadConfigDirectoryParser;
    private readonly DelayImportDirectoryParser _delayImportDirectoryParser;
    private readonly BoundImportDirectoryParser _boundImportDirectoryParser;
    private readonly ExceptionDirectoryParser _exceptionDirectoryParser;
    private readonly CertificateDirectoryParser _certificateDirectoryParser;
    private readonly ComDescriptorParser _comDescriptorParser;
    private readonly PeAnalysisOptions _options;

    public PeParser(
        DosHeaderParser dosHeaderParser,
        NtHeadersParser ntHeadersParser,
        SectionHeaderParser sectionHeaderParser,
        ImportDirectoryParser importDirectoryParser,
        ExportDirectoryParser exportDirectoryParser,
        ResourceDirectoryParser resourceDirectoryParser,
        RelocationDirectoryParser relocationDirectoryParser,
        DebugDirectoryParser debugDirectoryParser,
        TlsDirectoryParser tlsDirectoryParser,
        LoadConfigDirectoryParser loadConfigDirectoryParser,
        DelayImportDirectoryParser delayImportDirectoryParser,
        BoundImportDirectoryParser boundImportDirectoryParser,
        ExceptionDirectoryParser exceptionDirectoryParser,
        CertificateDirectoryParser certificateDirectoryParser,
        ComDescriptorParser comDescriptorParser,
        IOptions<PeAnalysisOptions> options)
    {
        _dosHeaderParser = dosHeaderParser;
        _ntHeadersParser = ntHeadersParser;
        _sectionHeaderParser = sectionHeaderParser;
        _importDirectoryParser = importDirectoryParser;
        _exportDirectoryParser = exportDirectoryParser;
        _resourceDirectoryParser = resourceDirectoryParser;
        _relocationDirectoryParser = relocationDirectoryParser;
        _debugDirectoryParser = debugDirectoryParser;
        _tlsDirectoryParser = tlsDirectoryParser;
        _loadConfigDirectoryParser = loadConfigDirectoryParser;
        _delayImportDirectoryParser = delayImportDirectoryParser;
        _boundImportDirectoryParser = boundImportDirectoryParser;
        _exceptionDirectoryParser = exceptionDirectoryParser;
        _certificateDirectoryParser = certificateDirectoryParser;
        _comDescriptorParser = comDescriptorParser;
        _options = options.Value;
    }

    public async Task<PeImageInfo> ParseAsync(Stream stream, CancellationToken cancellationToken)
    {
        BoundsChecker.EnsureSeekable(stream);

        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutCts.CancelAfter(_options.SafetyLimits.ProcessingTimeout);

        if (stream.Length > _options.SafetyLimits.MaxReadBufferSize)
        {
            throw new SafetyException($"Input exceeds configured max read buffer size ({_options.SafetyLimits.MaxReadBufferSize} bytes).");
        }

        var originalPosition = stream.Position;
        try
        {
            stream.Position = 0;
            using var memory = new MemoryStream();
            await stream.CopyToAsync(memory, 81920, timeoutCts.Token).ConfigureAwait(false);
            var rawBytes = memory.ToArray();

            using var ms = new MemoryStream(rawBytes, writable: false);
            using var reader = new BinaryReader(ms);

            var dosHeader = _dosHeaderParser.Parse(reader);
            if (dosHeader.Magic != 0x5A4D)
            {
                throw new SafetyException("Invalid DOS header magic.");
            }

            var dosStub = _dosHeaderParser.ParseDosStub(rawBytes, dosHeader.Lfanew);
            var richHeader = _dosHeaderParser.ParseRichHeader(rawBytes, dosHeader.Lfanew);
            var ntHeaders = _ntHeadersParser.Parse(reader, dosHeader);

            if (ntHeaders.Signature != 0x00004550)
            {
                throw new SafetyException("Invalid NT signature.");
            }

            var sectionHeadersOffset = dosHeader.Lfanew + 4 + 20 + ntHeaders.FileHeader.SizeOfOptionalHeader;
            if (sectionHeadersOffset < 0 || sectionHeadersOffset > rawBytes.Length)
            {
                throw new SafetyException("Invalid section header offset.");
            }

            ms.Position = sectionHeadersOffset;
            var sections = _sectionHeaderParser.Parse(reader, ntHeaders, _options.SafetyLimits, rawBytes);

            var imports = _importDirectoryParser.Parse(rawBytes, ntHeaders, sections, _options.SafetyLimits);
            var exports = _exportDirectoryParser.Parse(rawBytes, ntHeaders, sections, _options.SafetyLimits);
            var resources = _resourceDirectoryParser.Parse(rawBytes, ntHeaders, sections, _options.SafetyLimits);
            var relocations = _relocationDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var debugDirectories = _debugDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var tlsDirectory = _tlsDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var loadConfig = _loadConfigDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var delayImports = _delayImportDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var boundImports = _boundImportDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var exceptionEntries = _exceptionDirectoryParser.Parse(rawBytes, ntHeaders, sections);
            var certificates = _certificateDirectoryParser.Parse(rawBytes, ntHeaders);
            var comDescriptor = _comDescriptorParser.Parse(rawBytes, ntHeaders, sections);

            var overlayOffset = CalculateOverlayOffset(rawBytes, sections, ntHeaders.OptionalHeader.SizeOfHeaders);
            OverlayInfo? overlay = null;
            if (overlayOffset < rawBytes.Length)
            {
                var size = rawBytes.Length - overlayOffset;
                var overlayBytes = new byte[size];
                Buffer.BlockCopy(rawBytes, overlayOffset, overlayBytes, 0, size);
                overlay = new OverlayInfo(overlayOffset, size, overlayBytes);
            }

            var checksumValid = ValidateChecksum(rawBytes, ntHeaders.OptionalHeader.CheckSum);

            var family = DetectFamily(ntHeaders, sections, comDescriptor, imports, rawBytes);

            var warnings = new List<string>();
            if (overlay is not null)
            {
                warnings.Add("Overlay detected.");
            }

            if (!checksumValid)
            {
                warnings.Add("Checksum mismatch.");
            }

            return new PeImageInfo(
                rawBytes,
                dosHeader,
                dosStub,
                richHeader,
                ntHeaders,
                sections,
                imports,
                exports,
                resources,
                relocations,
                debugDirectories,
                tlsDirectory,
                loadConfig,
                delayImports,
                boundImports,
                exceptionEntries,
                certificates,
                comDescriptor,
                overlay,
                checksumValid,
                family,
                warnings);
        }
        finally
        {
            stream.Position = originalPosition;
        }
    }

    private static int CalculateOverlayOffset(byte[] rawBytes, IReadOnlyList<SectionInfo> sections, uint sizeOfHeaders)
    {
        var end = (int)sizeOfHeaders;
        foreach (var section in sections)
        {
            var candidate = checked((int)(section.PointerToRawData + section.SizeOfRawData));
            if (candidate > end)
            {
                end = candidate;
            }
        }

        return Math.Min(end, rawBytes.Length);
    }

    private static bool ValidateChecksum(byte[] rawBytes, uint expected)
    {
        unchecked
        {
            uint checksum = 0;
            var length = rawBytes.Length;
            for (var i = 0; i < length; i += 2)
            {
                ushort value = i + 1 < length
                    ? (ushort)(rawBytes[i] | rawBytes[i + 1] << 8)
                    : rawBytes[i];

                checksum = checksum + value & 0xFFFFFFFF;
                checksum = (checksum & 0xFFFF) + (checksum >> 16);
            }

            checksum = (checksum & 0xFFFF) + (checksum >> 16);
            checksum += (uint)length;
            return checksum == expected;
        }
    }

    private static ExecutableFamily DetectFamily(
        NtHeaders ntHeaders,
        IReadOnlyList<SectionInfo> sections,
        ComDescriptorInfo? comDescriptor,
        IReadOnlyList<ImportInfo> imports,
        byte[] rawBytes)
    {
        if (comDescriptor is not null)
        {
            return ExecutableFamily.DotNet;
        }

        if (sections.Count > 0)
        {
            var avgEntropy = 0d;
            foreach (var section in sections)
            {
                avgEntropy += section.Entropy;
            }

            avgEntropy /= sections.Count;
            if (avgEntropy > 7.2)
            {
                return ExecutableFamily.Packed;
            }
        }

        foreach (var import in imports)
        {
            if (import.DllName.Contains("kernel32", StringComparison.OrdinalIgnoreCase))
            {
                return ExecutableFamily.Native;
            }
        }

        return ExecutableFamily.Unknown;
    }
}