package builder

import (
	"bytes"
	"debug/elf"
	"debug/macho"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"runtime"
)

// ReadBuildID reads the build ID from the currently running executable.
func ReadBuildID() ([]byte, error) {
	executable, err := os.Executable()
	if err != nil {
		return nil, err
	}
	f, err := os.Open(executable)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	switch runtime.GOOS {
	case "linux", "freebsd":
		// Read the GNU build id section. (Not sure about FreeBSD though...)
		file, err := elf.NewFile(f)
		if err != nil {
			return nil, err
		}
		for _, section := range file.Sections {
			if section.Type != elf.SHT_NOTE || section.Name != ".note.gnu.build-id" {
				continue
			}
			buf := make([]byte, section.Size)
			n, err := section.ReadAt(buf, 0)
			if uint64(n) != section.Size || err != nil {
				return nil, fmt.Errorf("could not read build id: %w", err)
			}
			return buf, nil
		}
	case "darwin":
		// Read the LC_UUID load command, which contains the equivalent of a
		// build ID.
		file, err := macho.NewFile(f)
		if err != nil {
			return nil, err
		}
		for _, load := range file.Loads {
			// Unfortunately, the debug/macho package doesn't support the
			// LC_UUID command directly. So we have to read it from
			// macho.LoadBytes.
			load, ok := load.(macho.LoadBytes)
			if !ok {
				continue
			}
			raw := load.Raw()
			command := binary.LittleEndian.Uint32(raw)
			if command != 0x1b {
				// Looking for the LC_UUID load command.
				// LC_UUID is defined here as 0x1b:
				// https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
				continue
			}
			return raw[4:], nil
		}
	default:
		// On other platforms (such as Windows) there isn't such a convenient
		// build ID. Luckily, Go does have an equivalent of the build ID, which
		// is stored as a special symbol named go.buildid. You can read it
		// using `go tool buildid`, but the code below extracts it directly
		// from the binary.
		// Unfortunately, because of stripping with the -w flag, no symbol
		// table might be available. Therefore, we have to scan the binary
		// directly. Luckily the build ID is always at the start of the file.
		// For details, see:
		// https://github.com/golang/go/blob/master/src/cmd/internal/buildid/buildid.go
		fileStart := make([]byte, 4096)
		_, err := io.ReadFull(f, fileStart)
		index := bytes.Index(fileStart, []byte("\xff Go build ID: \""))
		if index < 0 || index > len(fileStart)-103 {
			return nil, fmt.Errorf("could not find build id in %s", err)
		}
		buf := fileStart[index : index+103]
		if bytes.HasPrefix(buf, []byte("\xff Go build ID: \"")) && bytes.HasSuffix(buf, []byte("\"\n \xff")) {
			return buf[len("\xff Go build ID: \"") : len(buf)-1], nil
		}
	}
	return nil, fmt.Errorf("could not find build ID in %s", executable)
}
