// Copyright (c) Huawei Technologies Co., Ltd. 2018-2019. All rights reserved.
// isulad-tools is licensed under the Mulan PSL v1.
// You can use this software according to the terms and conditions of the Mulan PSL v1.
// You may obtain a copy of Mulan PSL v1 at:
//    http://license.coscl.org.cn/MulanPSL
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
// PURPOSE.
// See the Mulan PSL v1 for more details.
// Description: virtual ethetic network driver
// Author: zhangwei
// Create: 2018-01-18

package veth

import (
	"fmt"
	"strings"
	"sync"

	"github.com/docker/libnetwork/netutils"
	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"

	"isula.org/isulad-tools/libnetwork/drivers/common"
	"isula.org/isulad-tools/libnetwork/nsutils"
	"isula.org/isulad-tools/pkg/ethtool"
)

type vethDriver struct {
	*common.Driver
	veth  *netlink.Veth
	mutex sync.Mutex
}

// New will create a veth driver
func New(d *common.Driver) (*vethDriver, error) {
	driver := &vethDriver{
		Driver: d,
	}
	return driver, nil
}

func (d *vethDriver) setDefaultVethFeature(name string) error {
	etool, err := ethtool.NewEthtool(name)
	if err != nil {
		return err
	}
	defer etool.Close()
	if err := etool.SetNetDeviceTSO(true); err != nil {
		logrus.Errorf("Failed to set device %s tso on with error: %v", name, err)
	}

	if err := etool.SetNetDeviceSG(true); err != nil {
		logrus.Errorf("Failed to set device %s sg on with error: %v", name, err)
	}

	if err := etool.SetNetDeviceTX(true); err != nil {
		logrus.Errorf("Failed to set device %s tx on with error: %v", name, err)
	}

	return nil
}

func (d *vethDriver) CreateIf() error {
	logrus.Debugf("creating veth pairs")
	hostIfName, err := netutils.GenerateIfaceName("veth", 10)
	if err != nil {
		return err
	}
	guestIfName, err := netutils.GenerateIfaceName("veth", 10)
	if err != nil {
		return err
	}
	// Generate and add the interface pipe host <-> sandbox
	d.mutex.Lock()
	d.veth = &netlink.Veth{
		LinkAttrs: netlink.LinkAttrs{Name: hostIfName, TxQLen: d.GetQlen()},
		PeerName:  guestIfName,
	}
	d.mutex.Unlock()
	if err = netlink.LinkAdd(d.veth); err != nil {
		return fmt.Errorf("failed to create veth pairs: %v", err)
	}
	if err := d.setDefaultVethFeature(guestIfName); err != nil {
		return err
	}
	if err := d.setDefaultVethFeature(hostIfName); err != nil {
		return err
	}
	logrus.Debugf("veth pair (%s, %s) created", hostIfName, guestIfName)
	return nil
}

func (d *vethDriver) DeleteIf() error {
	veth, err := netlink.LinkByName(d.GetHostNicName())
	if err != nil {
		// As add-nic supports 'update-config-only' option,
		// With this flag, isulad-tools will update config only, don't add device to container.
		// So if device dose not exist on host, ignore it.
		if strings.Contains(err.Error(), "Link not found") {
			return nil
		}
		return err
	}

	return netlink.LinkDel(veth)
}

func (d *vethDriver) setNicConfigure(nic netlink.Link) (rErr error) {
	// set MAC
	if d.GetMac() != nil {
		// set hardware address for interface
		if err := netlink.LinkSetHardwareAddr(nic, *(d.GetMac())); err != nil {
			return fmt.Errorf("failed to set hardware: %v", err)
		}
	}

	// set mtu
	if err := netlink.LinkSetMTU(nic, d.GetMtu()); err != nil {
		return fmt.Errorf("failed to set mtu: %v", err)
	}

	// set qlen
	if err := netlink.LinkSetTxQLen(nic, d.GetQlen()); err != nil {
		return fmt.Errorf("failed to set qlen(%d) for nic(%s)", d.GetQlen(), d.GetCtrNicName())
	}

	// set ipv4 address (TODO: ipv6 support?)
	oldAddr, _ := netlink.AddrList(nic, netlink.FAMILY_V4)
	if oldAddr != nil {
		// we only have on IP set for the interface
		if err := netlink.AddrDel(nic, &oldAddr[0]); err != nil {
			return fmt.Errorf("failed to delete old ip address: %v", err)
		}
	}
	ipAddr := &netlink.Addr{IPNet: d.GetIP(), Label: ""}
	if err := netlink.AddrAdd(nic, ipAddr); err != nil {
		return fmt.Errorf("failed to configure ip address: %v", err)
	}

	return nil
}

func (d *vethDriver) JoinAndConfigure() (rErr error) {
	if d.veth == nil || d.veth.Attrs() == nil {
		return fmt.Errorf("can't find veth interface")
	}
	// peerName does not matter, since we can delete veth pare via one end of it
	hostNicName := d.veth.Attrs().Name
	defer func() {
		if rErr != nil {
			logrus.Infof("Recover on failure: delete veth(%s)", hostNicName)
			nic, err := netlink.LinkByName(hostNicName)
			if err != nil {
				logrus.Errorf("Recover on failure: failed to get link by name(%q): %v", hostNicName, err)
				return
			}
			if err := netlink.LinkDel(nic); err != nil {
				logrus.Errorf("Recover on failure: failed to remove nic(%s): %v", hostNicName, err)
			}
		}
	}()
	err := nsutils.NsInvoke(
		d.GetNsPath(), func(nsFD int) error {
			// pre function is executed in host
			hostNic, err := netlink.LinkByName(d.veth.Attrs().Name)
			if err != nil {
				return fmt.Errorf("failed to get link by name %q: %v", d.veth.Attrs().Name, err)
			}
			ctrNic, err := netlink.LinkByName(d.veth.PeerName)
			if err != nil {
				return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err)
			}

			// down the interface before configuring
			if err := netlink.LinkSetDown(hostNic); err != nil {
				return fmt.Errorf("failed to set link down: %v", err)
			}
			if err := netlink.LinkSetDown(ctrNic); err != nil {
				return fmt.Errorf("failed to set link down: %v", err)
			}

			// move the network interface to the destination
			if err := netlink.LinkSetNsFd(ctrNic, nsFD); err != nil {
				return fmt.Errorf("failed to set namespace on link %q: %v", d.veth.PeerName, err)
			}

			// attach host nic to bridge and configure mtu
			if err = netlink.LinkSetMTU(hostNic, d.GetMtu()); err != nil {
				return fmt.Errorf("failed to set mtu: %v", err)
			}

			if d.GetHostNicName() != "" {
				// set iface to user desired name
				if err := netlink.LinkSetName(hostNic, d.GetHostNicName()); err != nil {
					return fmt.Errorf("failed to rename link %s -> %s: %v", d.veth.Attrs().Name, d.GetHostNicName(), err)
				}
				hostNicName = d.GetHostNicName()
				logrus.Debugf("Rename host link %s -> %s", d.veth.Attrs().Name, d.GetHostNicName())
			}

			if err = d.AddToBridge(); err != nil {
				return fmt.Errorf("failed to add to bridge: %v", err)
			}

			if err := netlink.LinkSetUp(hostNic); err != nil {
				return fmt.Errorf("failed to set link up: %v", err)
			}
			return nil
		}, func(nsFD int) error {
			// post function is executed in container
			ctrNic, err := netlink.LinkByName(d.veth.PeerName)
			if err != nil {
				return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err)
			}

			// set iface to user desired name
			if err := netlink.LinkSetName(ctrNic, d.GetCtrNicName()); err != nil {
				return fmt.Errorf("failed to rename link: %v", err)
			}
			logrus.Debugf("Rename container link %s -> %s", d.veth.PeerName, d.GetCtrNicName())

			if err := d.setNicConfigure(ctrNic); err != nil {
				return err
			}

			// Up the interface.
			if err := netlink.LinkSetUp(ctrNic); err != nil {
				return fmt.Errorf("failed to set link up: %v", err)
			}
			return nil
		})

	return err
}

func (d *vethDriver) Configure() (rErr error) {
	return nsutils.NsInvoke(
		d.GetNsPath(), func(nsFD int) error {
			// pre function is executed in host
			hostNic, err := netlink.LinkByName(d.GetHostNicName())
			if err != nil {
				return fmt.Errorf("failed to get link by name %q: %v", d.GetHostNicName(), err)
			}

			// down the interface before configuring
			if err := netlink.LinkSetDown(hostNic); err != nil {
				return fmt.Errorf("failed to set link down: %v", err)
			}

			// attach host nic to bridge and configure mtu
			if err = netlink.LinkSetMTU(hostNic, d.GetMtu()); err != nil {
				return fmt.Errorf("failed to set mtu: %v", err)
			}

			if err := netlink.LinkSetTxQLen(hostNic, d.GetQlen()); err != nil {
				return fmt.Errorf("failed to set qlen: %v", err)
			}

			if err = d.AddToBridge(); err != nil {
				return fmt.Errorf("failed to add to bridge: %v", err)
			}

			if err := netlink.LinkSetUp(hostNic); err != nil {
				return fmt.Errorf("failed to set link up: %v", err)
			}
			return nil
		}, func(nsFD int) error {
			// post function is executed in container
			ctrNic, err := netlink.LinkByName(d.GetCtrNicName())
			if err != nil {
				return fmt.Errorf("failed to get link by name %q: %v", d.GetCtrNicName(), err)
			}

			// down the interface before configuring
			if err := netlink.LinkSetDown(ctrNic); err != nil {
				return fmt.Errorf("failed to set link down: %v", err)
			}

			if err := d.setNicConfigure(ctrNic); err != nil {
				return err
			}

			// Up the interface.
			if err := netlink.LinkSetUp(ctrNic); err != nil {
				return fmt.Errorf("failed to set link up: %v", err)
			}
			return nil
		})

}

// AddTOBridge will add the veth to bridge
func (d *vethDriver) AddToBridge() error {
	if len(d.GetBridge()) == 0 {
		return fmt.Errorf("bridge can't be empty")
	}
	bd := d.GetBridgeDriver()
	if bd == nil {
		return fmt.Errorf("can't get bridge driver")
	}
	return bd.AddToBridge(d.GetHostNicName(), d.GetBridge())
}
