using System.Buffers.Binary;
using ZB.MOM.WW.MxGateway.Server.Configuration;

namespace ZB.MOM.WW.MxGateway.Server.Workers;

internal static class WorkerExecutableValidator
{
    private const ushort ImageFileMachineI386 = 0x014c;
    private const ushort ImageFileMachineAmd64 = 0x8664;
    private const int DosHeaderSignatureOffset = 0;
    private const int PeHeaderOffsetPointer = 0x3c;
    private const int PeSignatureSize = 4;
    private const int MachineOffsetFromPeHeader = PeSignatureSize;
    private const int MinimumHeaderSize = 0x40;

    /// <summary>Validates that a worker executable file has the required architecture.</summary>
    /// <param name="executablePath">Full path to the worker executable file.</param>
    /// <param name="requiredArchitecture">Required CPU architecture (x86 or x64).</param>
    /// <exception cref="WorkerProcessLaunchException">Thrown if the executable architecture does not match the required architecture.</exception>
    public static void Validate(
        string executablePath,
        WorkerArchitecture requiredArchitecture)
    {
        ushort machine = ReadMachineType(executablePath);
        ushort expectedMachine = requiredArchitecture switch
        {
            WorkerArchitecture.X86 => ImageFileMachineI386,
            WorkerArchitecture.X64 => ImageFileMachineAmd64,
            _ => throw new WorkerProcessLaunchException(
                WorkerProcessLaunchErrorCode.InvalidExecutable,
                "Worker executable required architecture is unsupported."),
        };

        if (machine != expectedMachine)
        {
            throw new WorkerProcessLaunchException(
                WorkerProcessLaunchErrorCode.InvalidExecutable,
                $"Worker executable architecture does not match required {requiredArchitecture} architecture.");
        }
    }

    /// <summary>Reads the PE machine type from the executable header.</summary>
    /// <param name="executablePath">Full path to the executable file.</param>
    /// <returns>Machine type constant from PE header.</returns>
    private static ushort ReadMachineType(string executablePath)
    {
        byte[] header = new byte[MinimumHeaderSize];
        using FileStream stream = File.OpenRead(executablePath);
        if (stream.Read(header) < header.Length)
        {
            throw InvalidExecutable("Worker executable is too small to contain a valid PE header.");
        }

        if (header[DosHeaderSignatureOffset] != 'M' || header[DosHeaderSignatureOffset + 1] != 'Z')
        {
            throw InvalidExecutable("Worker executable does not contain an MZ header.");
        }

        int peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan(PeHeaderOffsetPointer, sizeof(int)));
        if (peHeaderOffset < MinimumHeaderSize)
        {
            throw InvalidExecutable("Worker executable PE header offset is invalid.");
        }

        byte[] peHeaderBytes = new byte[PeSignatureSize + sizeof(ushort)];
        stream.Position = peHeaderOffset;
        if (stream.Read(peHeaderBytes) < peHeaderBytes.Length)
        {
            throw InvalidExecutable("Worker executable PE header is missing.");
        }

        if (peHeaderBytes[0] != 'P' || peHeaderBytes[1] != 'E' || peHeaderBytes[2] != 0 || peHeaderBytes[3] != 0)
        {
            throw InvalidExecutable("Worker executable does not contain a PE header.");
        }

        return BinaryPrimitives.ReadUInt16LittleEndian(
            peHeaderBytes.AsSpan(MachineOffsetFromPeHeader, sizeof(ushort)));
    }

    private static WorkerProcessLaunchException InvalidExecutable(string message)
    {
        return new WorkerProcessLaunchException(
            WorkerProcessLaunchErrorCode.InvalidExecutable,
            message);
    }
}
