// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build linux

package netlink

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"os"
	"syscall"

	"golang.org/x/sys/unix"

	info "github.com/google/cadvisor/info/v1"
)

var (
	// TODO(rjnagal): Verify and fix for other architectures.

	Endian = binary.LittleEndian
)

type genMsghdr struct {
	Command  uint8
	Version  uint8
	Reserved uint16
}

type netlinkMessage struct {
	Header    syscall.NlMsghdr
	GenHeader genMsghdr
	Data      []byte
}

func (m netlinkMessage) toRawMsg() (rawmsg syscall.NetlinkMessage) {
	rawmsg.Header = m.Header
	w := bytes.NewBuffer([]byte{})
	_ = binary.Write(w, Endian, m.GenHeader)
	w.Write(m.Data)
	rawmsg.Data = w.Bytes()
	return rawmsg
}

type loadStatsResp struct {
	Header    syscall.NlMsghdr
	GenHeader genMsghdr
	Stats     info.LoadStats
}

// Return required padding to align 'size' to 'alignment'.
func padding(size int, alignment int) int {
	unalignedPart := size % alignment
	return (alignment - unalignedPart) % alignment
}

// Get family id for taskstats subsystem.
func getFamilyID(conn *Connection) (uint16, error) {
	msg := prepareFamilyMessage()
	err := conn.WriteMessage(msg.toRawMsg())
	if err != nil {
		return 0, err
	}

	resp, err := conn.ReadMessage()
	if err != nil {
		return 0, err
	}
	id, err := parseFamilyResp(resp)
	if err != nil {
		return 0, err
	}
	return id, nil
}

// Append an attribute to the message.
// Adds attribute info (length and type), followed by the data and necessary padding.
// Can be called multiple times to add attributes. Only fixed size and string type
// attributes are handled. We don't need nested attributes for task stats.
func addAttribute(buf *bytes.Buffer, attrType uint16, data interface{}, dataSize int) {
	attr := syscall.RtAttr{
		Len:  syscall.SizeofRtAttr,
		Type: attrType,
	}
	attr.Len += uint16(dataSize)
	_ = binary.Write(buf, Endian, attr)
	switch data := data.(type) {
	case string:
		_ = binary.Write(buf, Endian, []byte(data))
		buf.WriteByte(0) // terminate
	default:
		_ = binary.Write(buf, Endian, data)
	}
	for i := 0; i < padding(int(attr.Len), syscall.NLMSG_ALIGNTO); i++ {
		buf.WriteByte(0)
	}
}

// Prepares the message and generic headers and appends attributes as data.
func prepareMessage(headerType uint16, cmd uint8, attributes []byte) (msg netlinkMessage) {
	msg.Header.Type = headerType
	msg.Header.Flags = syscall.NLM_F_REQUEST
	msg.GenHeader.Command = cmd
	msg.GenHeader.Version = 0x1
	msg.Data = attributes
	return msg
}

// Prepares message to query family id for task stats.
func prepareFamilyMessage() (msg netlinkMessage) {
	buf := bytes.NewBuffer([]byte{})
	addAttribute(buf, unix.CTRL_ATTR_FAMILY_NAME, unix.TASKSTATS_GENL_NAME, len(unix.TASKSTATS_GENL_NAME)+1)
	return prepareMessage(unix.GENL_ID_CTRL, unix.CTRL_CMD_GETFAMILY, buf.Bytes())
}

// Prepares message to query task stats for a task group.
func prepareCmdMessage(id uint16, cfd uintptr) (msg netlinkMessage) {
	buf := bytes.NewBuffer([]byte{})
	addAttribute(buf, unix.CGROUPSTATS_CMD_ATTR_FD, uint32(cfd), 4)
	return prepareMessage(id, unix.CGROUPSTATS_CMD_GET, buf.Bytes())
}

// Extracts returned family id from the response.
func parseFamilyResp(msg syscall.NetlinkMessage) (uint16, error) {
	m := new(netlinkMessage)
	m.Header = msg.Header
	err := verifyHeader(msg)
	if err != nil {
		return 0, err
	}
	buf := bytes.NewBuffer(msg.Data)
	// extract generic header from data.
	err = binary.Read(buf, Endian, &m.GenHeader)
	if err != nil {
		return 0, err
	}
	id := uint16(0)
	// Extract attributes. kernel reports family name, id, version, etc.
	// Scan till we find id.
	for buf.Len() > syscall.SizeofRtAttr {
		var attr syscall.RtAttr
		err = binary.Read(buf, Endian, &attr)
		if err != nil {
			return 0, err
		}
		if attr.Type == unix.CTRL_ATTR_FAMILY_ID {
			err = binary.Read(buf, Endian, &id)
			if err != nil {
				return 0, err
			}
			return id, nil
		}
		payload := int(attr.Len) - syscall.SizeofRtAttr
		skipLen := payload + padding(payload, syscall.SizeofRtAttr)
		name := make([]byte, skipLen)
		err = binary.Read(buf, Endian, name)
		if err != nil {
			return 0, err
		}
	}
	return 0, fmt.Errorf("family id not found in the response")
}

// Extract task stats from response returned by kernel.
func parseLoadStatsResp(msg syscall.NetlinkMessage) (*loadStatsResp, error) {
	m := new(loadStatsResp)
	m.Header = msg.Header
	err := verifyHeader(msg)
	if err != nil {
		return m, err
	}
	buf := bytes.NewBuffer(msg.Data)
	// Scan the general header.
	err = binary.Read(buf, Endian, &m.GenHeader)
	if err != nil {
		return m, err
	}
	// cgroup stats response should have just one attribute.
	// Read it directly into the stats structure.
	var attr syscall.RtAttr
	err = binary.Read(buf, Endian, &attr)
	if err != nil {
		return m, err
	}
	err = binary.Read(buf, Endian, &m.Stats)
	if err != nil {
		return m, err
	}
	return m, err
}

// Verify and return any error reported by kernel.
func verifyHeader(msg syscall.NetlinkMessage) error {
	switch msg.Header.Type {
	case syscall.NLMSG_DONE:
		return fmt.Errorf("expected a response, got nil")
	case syscall.NLMSG_ERROR:
		buf := bytes.NewBuffer(msg.Data)
		var errno int32
		err := binary.Read(buf, Endian, errno)
		if err != nil {
			return err
		}
		return fmt.Errorf("netlink request failed with error %s", syscall.Errno(-errno))
	}
	return nil
}

// Get load stats for a task group.
// id: family id for taskstats.
// cfd: open file to path to the cgroup directory under cpu hierarchy.
// conn: open netlink connection used to communicate with kernel.
func getLoadStats(id uint16, cfd *os.File, conn *Connection) (info.LoadStats, error) {
	msg := prepareCmdMessage(id, cfd.Fd())
	err := conn.WriteMessage(msg.toRawMsg())
	if err != nil {
		return info.LoadStats{}, err
	}

	resp, err := conn.ReadMessage()
	if err != nil {
		return info.LoadStats{}, err
	}

	parsedmsg, err := parseLoadStatsResp(resp)
	if err != nil {
		return info.LoadStats{}, err
	}
	return parsedmsg.Stats, nil
}
