// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// Build on Windows, Linux and *BSD

//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd

package posture

import (
	"fmt"
	"strings"

	"github.com/digitalocean/go-smbios/smbios"
	"github.com/sagernet/tailscale/types/logger"
)

// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
	// the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
	// so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
	index := specOffset - 4
	if index >= len(s.Formatted) || index < 0 {
		return 0
	}

	return s.Formatted[index]
}

// getStringFromSmbiosStructure retrieves a string at the given specOffset.
// Returns an empty string if no string was present.
func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) string {
	index := getByteFromSmbiosStructure(s, specOffset)

	if index == 0 || int(index) > len(s.Strings) {
		return ""
	}

	str := s.Strings[index-1]
	trimmed := strings.TrimSpace(str)

	return trimmed
}

// Product Table (Type 1) structure
// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
// Page 34 and onwards.
const (
	// Serial is present at the same offset in all IDs
	serialNumberOffset = 0x07

	productID   = 1
	baseboardID = 2
	chassisID   = 3
)

var (
	idToTableName = map[int]string{
		1: "product",
		2: "baseboard",
		3: "chassis",
	}
	validTables []string
	numOfTables int
)

func init() {
	for _, table := range idToTableName {
		validTables = append(validTables, table)
	}
	numOfTables = len(validTables)
}

func GetSerialNumbers(logf logger.Logf) ([]string, error) {
	// Find SMBIOS data in operating system-specific location.
	rc, _, err := smbios.Stream()
	if err != nil {
		return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
	}
	defer rc.Close()

	// Decode SMBIOS structures from the stream.
	d := smbios.NewDecoder(rc)
	ss, err := d.Decode()
	if err != nil {
		return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
	}

	serials := make([]string, 0, numOfTables)

	for _, s := range ss {
		switch s.Header.Type {
		case productID, baseboardID, chassisID:
			serial := getStringFromSmbiosStructure(s, serialNumberOffset)

			if serial != "" {
				serials = append(serials, serial)
			}
		}
	}
	return serials, nil
}
