// Copyright 2019 Yunion
//
// 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.

package sysutils

import (
	"fmt"
	"net"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"

	"yunion.io/x/onecloud/pkg/apis/compute"
	"yunion.io/x/onecloud/pkg/cloudcommon/types"
	"yunion.io/x/onecloud/pkg/compute/baremetal"
)

func valueOfKeyword(line string, key string) *string {
	lo := strings.ToLower(line)
	ko := strings.ToLower(key)
	pos := strings.Index(lo, ko)
	if pos >= 0 {
		val := strings.TrimSpace(line[pos+len(key):])
		return &val
	}
	return nil
}

func DumpMapToObject(data map[string]string, obj interface{}) error {
	return jsonutils.Marshal(data).Unmarshal(obj)
}

func ParseDMISysinfo(lines []string) (*types.SSystemInfo, error) {
	if len(lines) == 0 {
		return nil, fmt.Errorf("Empty input")
	}
	keys := map[string]string{
		"manufacture": "Manufacturer:",
		"model":       "Product Name:",
		"version":     "Version:",
		"sn":          "Serial Number:",
	}
	ret := make(map[string]string)
	for _, line := range lines {
		for key, keyword := range keys {
			val := valueOfKeyword(line, keyword)
			if val != nil {
				ret[key] = *val
			}
		}
	}
	info := types.SSystemInfo{}
	err := DumpMapToObject(ret, &info)
	if err != nil {
		return nil, err
	}
	if strings.ToLower(info.Version) == "none" {
		info.Version = ""
	}
	info.OemName = types.ManufactureOemName(info.Manufacture)
	return &info, nil
}

func ParseCPUInfo(lines []string) (*types.SCPUInfo, error) {
	cnt := 0
	var (
		model     string
		freq      string
		cache     string
		microcode string
		part      string
		revision  string
	)
	lv := func(line string) string {
		return strings.TrimSpace(line[strings.Index(line, ":")+1:])
	}
	for _, line := range lines {
		if len(model) == 0 && strings.HasPrefix(line, "model name") {
			model = lv(line)
		}
		if len(freq) == 0 && strings.HasPrefix(line, "cpu MHz") {
			freq = lv(line)
		}
		if len(cache) == 0 && strings.HasPrefix(line, "cache size") {
			cache = strings.TrimSpace(line[strings.Index(line, ":")+1 : strings.Index(line, " KB")])
		}
		if len(microcode) == 0 && strings.HasPrefix(line, "microcode") {
			microcode = lv(line)
		}
		if len(part) == 0 && strings.HasPrefix(line, "CPU part") {
			part = lv(line)
		}
		if len(revision) == 0 && strings.HasPrefix(line, "CPU revision") {
			revision = lv(line)
		}
		if strings.HasPrefix(line, "processor") {
			cnt += 1
		}
	}
	if len(model) == 0 {
		model = part
	}
	if len(model) == 0 {
		log.Errorf("Failed to get cpu model")
	}
	if len(freq) == 0 {
		log.Errorf("Failed to get cpu MHz")
	}
	if len(cache) == 0 {
		log.Errorf("Failed to get cpu cache size")
	}
	if len(microcode) == 0 {
		microcode = revision
	}
	model = strings.TrimSpace(model)
	info := &types.SCPUInfo{
		Count:     cnt,
		Model:     model,
		Microcode: microcode,
	}
	if len(cache) > 0 {
		info.Cache, _ = strconv.Atoi(cache)
	}
	if len(freq) > 0 {
		freqF, _ := strconv.ParseFloat(freq, 32)
		info.Freq = int(freqF)
	}
	return info, nil
}

func ParseDMICPUInfo(lines []string) *types.SDMICPUInfo {
	cnt := 0
	for _, line := range lines {
		if strings.HasPrefix(line, "Processor Information") {
			cnt += 1
		}
	}
	return &types.SDMICPUInfo{
		Nodes: cnt,
	}
}

func ParseDMIMemInfo(lines []string) *types.SDMIMemInfo {
	size := 0
	for _, line := range lines {
		val := valueOfKeyword(line, "Size:")
		if val == nil {
			continue
		}
		// skip 'Volatile Size:' line
		if strings.Contains(line, "Volatile") {
			continue
		}
		value := strings.ToLower(*val)
		if strings.HasSuffix(value, " mb") {
			sizeMb, err := strconv.Atoi(strings.TrimSuffix(value, " mb"))
			if err != nil {
				log.Errorf("parse MB error: %v", err)
				continue
			}
			size += sizeMb
		} else if strings.HasSuffix(value, " gb") {
			sizeGb, err := strconv.Atoi(strings.TrimSuffix(value, " gb"))
			if err != nil {
				log.Errorf("parse GB error: %v", err)
				continue
			}
			size += sizeGb * 1024
		}
	}
	return &types.SDMIMemInfo{Total: size}
}

func ParseDMIIPMIInfo(lines []string) bool {
	for _, line := range lines {
		val := valueOfKeyword(line, "Interface Type:")
		if val != nil {
			return true
		}
	}
	return false
}

func ParseNicInfo(lines []string) []*types.SNicDevInfo {
	ret := make([]*types.SNicDevInfo, 0)
	for _, line := range lines {
		dat := strings.Split(line, " ")
		if len(dat) > 4 {
			dev := dat[0]
			mac, _ := net.ParseMAC(dat[1])
			speed, _ := strconv.Atoi(dat[2])
			up := false
			if dat[3] == "1" {
				up = true
			}
			mtu, _ := strconv.Atoi(dat[4])
			ret = append(ret, &types.SNicDevInfo{
				Dev:   dev,
				Mac:   mac,
				Speed: speed,
				Up:    &up,
				Mtu:   mtu,
			})
		}
	}
	return ret
}

func ParseDiskInfo(lines []string, driver string) []*types.SDiskInfo {
	ret := make([]*types.SDiskInfo, 0)
	for _, line := range lines {
		data := strings.Split(line, " ")
		if len(data) <= 6 {
			continue
		}
		dev := data[0]
		sector, _ := strconv.Atoi(data[1])
		size := sector * 512 / 1024 / 1024
		block, _ := strconv.Atoi(data[2])
		rotate := false
		if data[3] == "1" {
			rotate = true
		}
		kernel := data[4]
		pciCls := data[5]
		modinfo := strings.Join(data[6:], " ")
		ret = append(ret, &types.SDiskInfo{
			Dev:        dev,
			Sector:     int64(sector),
			Block:      int64(block),
			Size:       int64(size),
			Rotate:     rotate,
			ModuleInfo: modinfo,
			Kernel:     kernel,
			PCIClass:   pciCls,
			Driver:     driver,
		})
	}
	return ret
}

func ParsePCIEDiskInfo(lines []string) []*types.SDiskInfo {
	return ParseDiskInfo(lines, baremetal.DISK_DRIVER_PCIE)
}

func ParseSCSIDiskInfo(lines []string) []*types.SDiskInfo {
	return ParseDiskInfo(lines, baremetal.DISK_DRIVER_LINUX)
}

func GetSecureTTYs(lines []string) []string {
	ttys := []string{}
	for _, l := range lines {
		if len(l) == 0 {
			continue
		}
		if strings.HasPrefix(l, "#") {
			continue
		}
		ttys = append(ttys, l)
	}
	return ttys
}

func GetSerialPorts(lines []string) []string {
	// http://wiki.networksecuritytoolkit.org/index.php/Console_Output_and_Serial_Terminals
	ret := []string{}
	for _, l := range lines {
		if strings.Contains(l, "CTS") || strings.Contains(l, "RTS") {
			pos := strings.Index(l, ":")
			if pos < 0 {
				continue
			}
			idx := l[0:pos]
			ret = append(ret, fmt.Sprintf("ttyS%s", idx))
		}
	}
	return ret
}

// ParseSGMap parse command 'sg_map -x' outputs:
//
//	/dev/sg1 0 2 0 0 0 /dev/sda
//	/dev/sg2 0 2 1 0 0 /dev/sdb
//	/dev/sg3 0 2 2 0 0 /dev/sdc
func ParseSGMap(lines []string) []compute.SGMapItem {
	ret := make([]compute.SGMapItem, 0)
	for _, l := range lines {
		items := strings.Fields(l)
		if len(items) != 7 {
			continue
		}
		hostNum, _ := strconv.Atoi(items[1])
		bus, _ := strconv.Atoi(items[2])
		scsiId, _ := strconv.Atoi(items[3])
		lun, _ := strconv.Atoi(items[4])
		typ, _ := strconv.Atoi(items[5])
		ret = append(ret, compute.SGMapItem{
			SGDeviceName:    items[0],
			HostNumber:      hostNum,
			Bus:             bus,
			SCSIId:          scsiId,
			Lun:             lun,
			Type:            typ,
			LinuxDeviceName: items[6],
		})
	}
	return ret
}

func Start(closeFd bool, args ...string) (p *os.Process, err error) {
	if args[0], err = exec.LookPath(args[0]); err == nil {
		var procAttr os.ProcAttr
		if closeFd {
			procAttr.Files = []*os.File{nil, nil, nil}
		} else {
			procAttr.Files = []*os.File{os.Stdin,
				os.Stdout, os.Stderr}
		}
		p, err := os.StartProcess(args[0], args, &procAttr)
		if err == nil {
			return p, nil
		}
	}
	return nil, err
}

func ParseIPMIUser(lines []string) []compute.IPMIUser {
	ret := make([]compute.IPMIUser, 0)
	for _, l := range lines {
		if strings.HasPrefix(l, "ID") {
			continue
		}
		fields := strings.Fields(l)
		if strings.Contains(l, "Empty User") {
			id, err := strconv.Atoi(fields[0])
			if err != nil {
				continue
			}
			ret = append(ret, compute.IPMIUser{Id: id})
			continue
		}
		if len(fields) != 6 {
			continue
		}
		id, err := strconv.Atoi(fields[0])
		if err != nil {
			continue
		}
		ret = append(ret, compute.IPMIUser{
			Id:   id,
			Name: fields[1],
			Priv: fields[5],
		})
	}
	return ret
}

const (
	UUID_EMPTY = "00000000-0000-0000-0000-000000000000"
)

func NormalizeUuid(uuid string) string {
	uuid = strings.ToLower(uuid)
	if uuid == UUID_EMPTY {
		uuid = ""
	}
	return uuid
}