// 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 ipmitool

import (
	"context"
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"

	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/tristate"
	"yunion.io/x/pkg/util/stringutils"
	"yunion.io/x/pkg/utils"

	"yunion.io/x/onecloud/pkg/apis/compute"
	"yunion.io/x/onecloud/pkg/cloudcommon/types"
	"yunion.io/x/onecloud/pkg/util/procutils"
	"yunion.io/x/onecloud/pkg/util/ssh"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
	"yunion.io/x/onecloud/pkg/util/sysutils"
)

type IPMIParser struct{}

func (parser *IPMIParser) GetDefaultTimeout() time.Duration {
	return 20 * time.Second
}

var (
	BOOTDEVS = []string{"pxe", "disk", "safe", "diag", "cdrom", "bios"}
	SOLOPTS  = []string{"default", "skip", "enable"}
)

type Args []string

func newArgs(args ...interface{}) Args {
	ret := make([]string, len(args))
	for i, arg := range args {
		ret[i] = fmt.Sprintf("%v", arg)
	}
	return ret
}

type IPMIExecutor interface {
	GetMode() string
	ExecuteCommand(args ...string) ([]string, error)
}

type SSHIPMI struct {
	IPMIParser
	sshClient *ssh.Client
}

func NewSSHIPMI(cli *ssh.Client) *SSHIPMI {
	return &SSHIPMI{
		sshClient: cli,
	}
}

func (ipmi *SSHIPMI) GetMode() string {
	return "ssh"
}

func (ipmi *SSHIPMI) GetCommand(args ...string) *procutils.Command {
	nArgs := []string{"-I", "open"}
	nArgs = append(nArgs, args...)
	return procutils.NewCommand("/usr/bin/ipmitool", nArgs...)
}

func (ipmi *SSHIPMI) ExecuteCommand(args ...string) ([]string, error) {
	cmd := ipmi.GetCommand(args...)
	log.Debugf("[SSHIPMI] execute command: %s", cmd)
	return ipmi.sshClient.Run(cmd.String())
}

type LanPlusIPMI struct {
	IPMIParser
	host     string
	user     string
	password string
	port     int
}

func NewLanPlusIPMI(host, user, password string) *LanPlusIPMI {
	return NewLanPlusIPMIWithPort(host, user, password, 623)
}

func NewLanPlusIPMIWithPort(host, user, password string, port int) *LanPlusIPMI {
	return &LanPlusIPMI{
		host:     host,
		user:     user,
		password: password,
		port:     port,
	}
}

func (ipmi *LanPlusIPMI) GetMode() string {
	return "rmcp"
}

func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context.CancelFunc) {
	nArgs := []string{
		"-I", "lanplus", "-H", ipmi.host,
		"-p", fmt.Sprintf("%d", ipmi.port),
		"-U", ipmi.user,
		"-P", ipmi.password,
	}
	nArgs = append(nArgs, args...)
	ctx, cancel := context.WithTimeout(context.Background(), ipmi.GetDefaultTimeout())
	return procutils.NewCommandContext(ctx, "ipmitool", nArgs...), cancel
}

func (ipmi *LanPlusIPMI) ExecuteCommand(args ...string) ([]string, error) {
	cmd, cancel := ipmi.GetCommand(args...)
	defer cancel()
	log.Debugf("[LanPlusIPMI] execute command: %s", cmd.String())
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	return ssh.ParseOutput(out), nil
}

func GetSysGuid(exector IPMIExecutor) string {
	args := []string{"mc", "guid"}
	// args := []string{"raw", "0x06", "0x37"}
	lines, err := exector.ExecuteCommand(args...)
	if err != nil {
		// ignore error
		log.Errorf("mc guid error: %s", err)
	}
	for _, line := range lines {
		key, val := stringutils.SplitKeyValue(line)
		if key == "System GUID" {
			return sysutils.NormalizeUuid(val)
		}
	}
	return ""
}

func GetSysInfo(exector IPMIExecutor) (*types.SSystemInfo, error) {
	// TODO: do cache
	args := []string{"fru", "print", "0"}
	lines, err := exector.ExecuteCommand(args...)
	if err != nil {
		// ignore error
		log.Errorf("fru print 0 error: %s", err)
	}
	ret := make(map[string]string)

	keys := map[string]string{
		"manufacture": "Product Manufacturer",
		"model":       "Product Name",
		"bmodel":      "Board Product",
		"version":     "Product Version",
		"sn":          "Product Serial",
		"bsn":         "Board Serial",
	}

	for _, line := range lines {
		key, val := stringutils.SplitKeyValue(line)
		if key != "" {
			for n, v := range keys {
				if _, ok := ret[n]; v == key && !ok {
					ret[n] = val
				}
			}
		}
	}
	_, snOk := ret["sn"]
	bsn, bsnOk := ret["bsn"]
	if !snOk && bsnOk {
		// no product serial
		ret["sn"] = bsn
	}
	info := types.SSystemInfo{}
	err = sysutils.DumpMapToObject(ret, &info)
	info.OemName = types.ManufactureOemName(info.Manufacture)
	return &info, err
}

/*func GetLanChannels(sysinfo *types.SSystemInfo) []int {
	return profiles.GetLanChannel(sysinfo)
}

func GetDefaultLanChannel(sysinfo *types.SSystemInfo) int {
	return GetLanChannels(sysinfo)[0]
}

func GetRootId(sysinfo *types.SSystemInfo) int {
	return profiles.GetRootId(sysinfo)
}*/

func GetLanConfig(exector IPMIExecutor, channel uint8) (*types.SIPMILanConfig, error) {
	args := newArgs("lan", "print", channel)
	lines, err := ExecuteCommands(exector, args)
	if err != nil {
		return nil, err
	}
	ret := new(types.SIPMILanConfig)
	for _, line := range lines {
		key, val := stringutils.SplitKeyValue(line)
		if key == "" {
			continue
		}
		switch key {
		case "IP Address Source":
			if val == "Static Address" {
				ret.IPSrc = "static"
			}
		case "IP Address":
			ret.IPAddr = val
		case "Subnet Mask":
			ret.Netmask = val
		case "MAC Address":
			ret.Mac, _ = net.ParseMAC(val)
		case "Default Gateway IP":
			ret.Gateway = val
		case "802.1q VLAN ID":
			vlanId, _ := strconv.ParseInt(val, 10, 64)
			ret.VlanId = int(vlanId)
		}
	}
	return ret, nil
}

func tryExecuteCommand(exector IPMIExecutor, args ...string) ([]string, error) {
	var err error
	var ret []string
	maxTries := 3
	for tried := 0; tried < maxTries; tried++ {
		ret, err = exector.ExecuteCommand(args...)
		if err == nil {
			return ret, nil
		}
		sleepTime := time.Second * (1 << uint(tried))
		log.Errorf("Execute args %v error: %v, sleep %s then try again", args, err, sleepTime)
		time.Sleep(sleepTime)
	}
	return ret, err
}

func ExecuteCommands(exector IPMIExecutor, args ...Args) ([]string, error) {
	results := make([]string, 0)
	for _, arg := range args {
		ret, err := tryExecuteCommand(exector, arg...)
		if err != nil {
			return nil, err
		}
		results = append(results, ret...)
	}
	return results, nil
}

func doActions(exector IPMIExecutor, actionName string, args ...Args) error {
	_, err := ExecuteCommands(exector, args...)
	if err != nil {
		return fmt.Errorf("Do %s action error: %v", actionName, err)
	}
	return nil
}

func SetLanDHCP(exector IPMIExecutor, lanChannel uint8) error {
	args := newArgs("lan", "set", lanChannel, "ipsrc", "dhcp")
	return doActions(exector, "set_lan_dhcp", args)
}

func SetLanStatic(
	exector IPMIExecutor,
	channel uint8,
	ip string,
	mask string,
	gateway string,
) error {
	// config, err := GetLanConfig(exector, channel)
	// if err != nil {
	// 	return err
	// }
	// var argss []Args
	// if config.IPAddr == ip && config.Netmask == mask && config.Gateway == gateway {
	argss := []Args{
		newArgs("lan", "set", channel, "ipsrc", "static"),
		newArgs("lan", "set", channel, "ipaddr", ip),
		newArgs("lan", "set", channel, "netmask", mask),
		newArgs("lan", "set", channel, "defgw", "ipaddr", gateway),
	}
	// } else {
	//	argss = []Args{
	//		newArgs("lan", "set", channel, "ipsrc", "static"),
	//		newArgs("lan", "set", channel, "ipaddr", ip),
	//		newArgs("lan", "set", channel, "netmask", mask),
	//		newArgs("lan", "set", channel, "defgw", "ipaddr", gateway),
	//	}
	// }
	return doActions(exector, "set_lan_static", argss...)
}

func SetLanStaticIP(exector IPMIExecutor, channel uint8, ip string) error {
	args := newArgs("lan", "set", channel, "ipaddr", ip)
	return doActions(exector, "set_lan_static_ip", args)
}

func setLanAccess(exector IPMIExecutor, channel uint8, access string) error {
	args := []Args{
		newArgs("lan", "set", channel, "access", access),
		// newArgs("lan", "set", channel, "auth", "ADMIN", "MD5"),
	}
	return doActions(exector, "set_lan_access", args...)
}

func EnableLanAccess(exector IPMIExecutor, channel uint8) error {
	return setLanAccess(exector, channel, "on")
}

func ListLanUsers(exector IPMIExecutor, channel uint8) ([]compute.IPMIUser, error) {
	args := newArgs("user", "list", channel)
	ret, err := ExecuteCommands(exector, args)
	if err != nil {
		return nil, errors.Wrapf(err, "list user at channel %d", channel)
	}
	return sysutils.ParseIPMIUser(ret), nil
}

func CreateOrSetAdminUser(exector IPMIExecutor, channel uint8, rootId int, username string, password string) error {
	users, err := ListLanUsers(exector, channel)
	if err != nil {
		return errors.Wrap(err, "List users")
	}
	if len(users) == 0 {
		return errors.Errorf("Empty users at channel %d", channel)
	}
	var foundUser *compute.IPMIUser = nil
	for _, user := range users {
		tmp := user
		if user.Name == username {
			foundUser = &tmp
			break
		}
	}
	if foundUser == nil {
		minEmptyUserId := -1
		for _, user := range users {
			if user.Name == "" {
				minEmptyUserId = user.Id
				break
			}
		}
		if minEmptyUserId == -1 {
			log.Warningf("Not found min empty user id, use root id %d to set", rootId)
			minEmptyUserId = rootId
		}
		return SetIdUserPasswd(exector, channel, minEmptyUserId, username, password)
	}
	return SetLanUserAdminPasswd(exector, channel, foundUser.Id, password)
}

func SetLanUserAdminPasswd(exector IPMIExecutor, channel uint8, id int, password string) error {
	var err error
	password, err = stringutils2.EscapeEchoString(password)
	if err != nil {
		return fmt.Errorf("EscapeEchoString for password: %s, error: %v", password, err)
	}
	args := []Args{
		newArgs("user", "enable", id),
		newArgs("user", "set", "password", id, fmt.Sprintf("\"%s\"", password)),
		newArgs("user", "priv", id, 4, channel),
	}
	err = doActions(exector, "set_lan_user_password", args...)
	if err != nil {
		return err
	}
	args = []Args{newArgs(
		"raw", "0x06", "0x43",
		fmt.Sprintf("0x%02x", 0xb0+channel),
		fmt.Sprintf("0x%02x", id), "0x04", "0x00")}
	err = doActions(exector, "set_lan_user_password2", args...)
	if err == nil {
		return nil
	}
	args = []Args{newArgs(
		"channel", "setaccess", channel,
		id, "link=on", "ipmi=on",
		"callin=on", "privilege=4",
	)}
	return doActions(exector, "set_lan_user_password3", args...)
}

func SetIdUserPasswd(exector IPMIExecutor, channel uint8, id int, user string, password string) error {
	args := newArgs("user", "set", "name", id, user)
	if err := doActions(exector, fmt.Sprintf("set_id%d_name", id), args); err != nil {
		return errors.Wrapf(err, "change root id %d to name %s", id, user)
	}
	return SetLanUserAdminPasswd(exector, channel, id, password)
}

func SetLanPasswd(exector IPMIExecutor, rootId int, password string) error {
	password, err := stringutils2.EscapeEchoString(password)
	if err != nil {
		return fmt.Errorf("EscapeEchoString for password: %v", err)
	}
	args := newArgs("user", "set", "password", rootId, fmt.Sprintf("\"%s\"", password))
	return doActions(exector, "set_lan_passwd", args)
}

func GetChassisPowerStatus(exector IPMIExecutor) (string, error) {
	args := newArgs("chassis", "power", "status")
	ret, err := ExecuteCommands(exector, args)
	if err != nil {
		return "", err
	}
	for _, line := range ret {
		if strings.Contains(line, "Chassis Power is") {
			data := strings.Split(line, " ")
			status := strings.ToLower(strings.TrimSpace(data[len(data)-1]))
			return status, nil
		}
	}
	return "", fmt.Errorf("Unknown chassis status")
}

func GetBootFlags(exector IPMIExecutor) (*types.SIPMIBootFlags, error) {
	args := newArgs("raw", "0x00", "0x09", "0x05", "0x00", "0x00")
	ret, err := ExecuteCommands(exector, args)
	if err != nil {
		return nil, err
	}
	bytes, err := HexStr2Bytes(ret[0])
	if err != nil {
		return nil, err
	}
	bootdevIdx := ((bytes[3] >> 2) & 0x0f) - 1
	bootdev := ""
	if bootdevIdx >= 0 && int(bootdevIdx) < len(BOOTDEVS) {
		bootdev = BOOTDEVS[bootdevIdx]
	}
	flags := &types.SIPMIBootFlags{
		Dev: bootdev,
	}
	solIdx := (bytes[4] & 0x03)
	if solIdx == 1 {
		sol := false
		flags.Sol = &sol
	} else if solIdx == 2 {
		sol := true
		flags.Sol = &sol
	}
	return flags, nil
}

func HexStr2Bytes(hs string) ([]int64, error) {
	b := []int64{}
	for _, x := range strings.Split(hs, " ") {
		intV, err := strconv.ParseInt(x, 16, 64)
		if err != nil {
			return nil, err
		}
		b = append(b, intV)
	}
	return b, nil
}

func GetACPIPowerStatus(exector IPMIExecutor) ([]int64, error) {
	args := newArgs("raw", "0x06", "0x07")
	ret, err := ExecuteCommands(exector, args)
	if err != nil {
		return nil, err
	}
	return HexStr2Bytes(ret[0])
}

func DoSoftShutdown(exector IPMIExecutor) error {
	args := newArgs("chassis", "power", "soft")
	return doActions(exector, "do_soft_shutdown", args)
}

func DoHardShutdown(exector IPMIExecutor) error {
	args := newArgs("chassis", "power", "off")
	return doActions(exector, "do_hard_shutdown", args)
}

func DoPowerOn(exector IPMIExecutor) error {
	args := newArgs("chassis", "power", "on")
	return doActions(exector, "do_power_on", args)
}

func DoPowerReset(exector IPMIExecutor) error {
	args := newArgs("chassis", "power", "reset")
	return doActions(exector, "do_power_reset", args)
}

func DoPowerCycle(exector IPMIExecutor) error {
	args := newArgs("chassis", "power", "cycle")
	return doActions(exector, "do_power_cycle", args)
}

func DoReboot(exector IPMIExecutor) error {
	maxTries := 10

	var status string
	var err error
	status, err = GetChassisPowerStatus(exector)
	if err != nil {
		log.Errorf("DoReboot get power status 1st: %v", err)
	}

	isValidStatus := func(s string) bool {
		return utils.IsInStringArray(s, []string{string(types.POWER_STATUS_ON), string(types.POWER_STATUS_OFF)})
	}

	for tried := 0; !isValidStatus(status) && tried <= maxTries; tried++ {
		time.Sleep(1 * time.Second)
		status, err = GetChassisPowerStatus(exector)
		if err != nil {
			log.Errorf("DoReboot %d tries to get power status: %v", tried, err)
		}
	}

	if !isValidStatus(status) {
		return fmt.Errorf("Unexpected power status: %q", status)
	}

	// do shutdown
	if status == string(types.POWER_STATUS_ON) {
		if err := DoHardShutdown(exector); err != nil {
			log.Errorf("DoHardShutdown: %v", err)
		}
		time.Sleep(1 * time.Second)
		for tried := 0; tried < maxTries; tried++ {
			status, err = GetChassisPowerStatus(exector)
			if err != nil {
				log.Errorf("DoReboot %d tries to get power status: %v", tried, err)
			}
			if status == string(types.POWER_STATUS_OFF) {
				break
			}
			time.Sleep(10 * time.Second)
		}
	}

	// do power on
	status, _ = GetChassisPowerStatus(exector)
	for tried := 0; status != string(types.POWER_STATUS_ON) && tried < maxTries; tried++ {
		if err := DoPowerOn(exector); err != nil {
			log.Errorf("DoReboot %d tries to power on: %v", tried, err)
		}
		time.Sleep(10 * time.Second)
		status, err = GetChassisPowerStatus(exector)
		if err != nil {
			log.Errorf("DoReboot %d tries to get power status: %v", tried, err)
		}
	}

	status, err = GetChassisPowerStatus(exector)
	if err != nil {
		return errors.Wrap(err, "Get power status after power on")
	}
	if status != string(types.POWER_STATUS_ON) {
		return errors.Errorf("do reboot fail to poweron, current status: %s", status)
	}
	return nil
}

func doRebootToFlag(exector IPMIExecutor, setFunc func(IPMIExecutor) error) error {
	err := setFunc(exector)
	if err != nil {
		return err
	}
	return DoReboot(exector)
}

func SetRebootToDisk(exector IPMIExecutor) error {
	return SetBootFlags(exector, "disk", tristate.True, true)
}

func DoRebootToDisk(exector IPMIExecutor) error {
	return doRebootToFlag(exector, SetRebootToDisk)
}

func SetRebootToPXE(exector IPMIExecutor) error {
	return SetBootFlagPXE(exector)
}

func DoRebootToPXE(exector IPMIExecutor) error {
	return doRebootToFlag(exector, SetRebootToPXE)
}

func SetRebootToBIOS(exector IPMIExecutor) error {
	return SetBootFlags(exector, "bios", tristate.True, false)
}

func DoRebootToBIOS(exector IPMIExecutor) error {
	return doRebootToFlag(exector, SetRebootToBIOS)
}

func SetBootFlagPXE(exector IPMIExecutor) error {
	return setBootFlagsV2(exector, "pxe")
}

func SetBootFlags(
	exector IPMIExecutor,
	bootdev string,
	sol tristate.TriState,
	enablePersistent bool,
) error {
	err := setBootFlagsV1(exector, bootdev, sol, enablePersistent)
	if err == nil {
		return nil
	}
	return setBootFlagsV2(exector, bootdev)
}

func setBootFlagsV1(
	exector IPMIExecutor,
	bootdev string,
	sol tristate.TriState,
	enablePersistent bool,
) error {
	cmd := []interface{}{"raw", "0x00", "0x08", "0x05"}
	bootdevIdx := 0
	if ok, idx := utils.InStringArray(bootdev, BOOTDEVS); ok {
		bootdevIdx = idx + 1
	} else {
		return fmt.Errorf("Illegal bootdev %s", bootdev)
	}
	valid := 0x80
	if enablePersistent {
		valid = valid + 0x40
	}
	solIdx := 0
	if !sol.IsNone() {
		if sol.IsTrue() {
			solIdx = 2
		} else {
			solIdx = 1
		}
	}
	for _, x := range []int{valid, bootdevIdx << 2, solIdx, 0, 0} {
		cmd = append(cmd, fmt.Sprintf("0x%02x", x))
	}
	return doActions(exector, "set_boot_flags_v1", newArgs(cmd...))
}

func setBootFlagsV2(exector IPMIExecutor, bootdev string) error {
	return doActions(
		exector,
		fmt.Sprintf("set_boot_flag_%s", bootdev),
		newArgs("chassis", "bootdev", bootdev),
	)
}

func GetIPMILanPort(exector IPMIExecutor) (string, error) {
	ret, err := ExecuteCommands(exector, newArgs("delloem", "lan", "get"))
	if err != nil {
		return "", err
	}
	return ret[1], nil
}

func SetDellIPMILanPortShared(exector IPMIExecutor) error {
	args1 := newArgs("delloem", "lan", "set", "shared")
	args2 := newArgs("delloem", "lan", "set", "shared", "with", "lom1")
	err2 := doActions(exector, "_dell_set_ipmi_lan_port_shared_02", args2)
	if err2 != nil {
		return doActions(exector, "_dell_set_ipmi_lan_port_shared_01", args1)
	}
	return nil
}

func SetHuaweiIPMILanPortShared(exector IPMIExecutor) error {
	args := []Args{
		newArgs(
			"raw", "0xc", "0x1", "0x1", "0xd7", "0xdb",
			"0x07", "0x00", "0x2",
		),
		newArgs(
			"raw", "0x30", "0x93", "0xdb", "0x07", "0x00",
			"0x05", "0x0d", "0x0", "0x0", "0x1", "0x0",
		),
	}
	return doActions(exector, "_huawei_set_ipmi_lan_port_shared", args...)
}

func SetIPMILanPortDedicated(exector IPMIExecutor) error {
	return doActions(
		exector,
		"set_ipmi_lan_port_dedicated",
		newArgs("delloem", "lan", "set", "dedicated"),
	)
}

func DoBMCReset(exector IPMIExecutor) error {
	return doActions(exector, "do_bmc_reset", newArgs("mc", "reset", "cold"))
}
