// Copyright 2019 The Prometheus Authors
//
// 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 iscsi

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/prometheus/procfs/internal/util"
)

// GetStats is the main iscsi status information func
// building the path and prepare info for enable iscsi
func GetStats(iqnPath string) (*Stats, error) {
	var istats Stats

	istats.Name = filepath.Base(iqnPath)
	istats.RootPath = filepath.Dir(iqnPath)

	matches, err := filepath.Glob(filepath.Join(iqnPath, "tpgt*"))
	if err != nil {
		return nil, fmt.Errorf("iscsi: GetStats: get TPGT path error %w", err)
	}
	istats.Tpgt = make([]TPGT, len(matches))

	for i, tpgtPath := range matches {
		istats.Tpgt[i].Name = filepath.Base(tpgtPath)
		istats.Tpgt[i].TpgtPath = tpgtPath
		istats.Tpgt[i].IsEnable, _ = isPathEnable(tpgtPath)
		if istats.Tpgt[i].IsEnable {
			matchesLunsPath, _ := filepath.Glob(filepath.Join(tpgtPath, "lun/lun*"))

			for _, lunPath := range matchesLunsPath {
				lun, err := getLunLinkTarget(lunPath)
				if err == nil {
					istats.Tpgt[i].Luns = append(istats.Tpgt[i].Luns, lun)
				}
			}
		}
	}
	return &istats, nil
}

// isPathEnable is a utility function
// check if the file "enable" contain enable message
func isPathEnable(path string) (bool, error) {
	enableReadout, err := ioutil.ReadFile(filepath.Join(path, "enable"))
	if err != nil {
		return false, fmt.Errorf("iscsi: isPathEnable ReadFile error %w", err)
	}
	isEnable, err := strconv.ParseBool(strings.TrimSpace(string(enableReadout)))
	if err != nil {
		return false, fmt.Errorf("iscsi: isPathEnable ParseBool error %w", err)
	}
	return isEnable, nil
}

func getLunLinkTarget(lunPath string) (lunObject LUN, err error) {
	lunObject.Name = filepath.Base(lunPath)
	lunObject.LunPath = lunPath
	files, err := ioutil.ReadDir(lunPath)
	if err != nil {
		return lunObject, fmt.Errorf("getLunLinkTarget: ReadDir path %q: %w", lunPath, err)
	}
	for _, file := range files {
		fileInfo, _ := os.Lstat(filepath.Join(lunPath, file.Name()))
		if fileInfo.Mode()&os.ModeSymlink != 0 {
			target, err := os.Readlink(filepath.Join(lunPath, fileInfo.Name()))
			if err != nil {
				return lunObject, fmt.Errorf("getLunLinkTarget: Readlink: %w", err)
			}
			targetPath, objectName := filepath.Split(target)
			_, typeWithNumber := filepath.Split(filepath.Clean(targetPath))

			underscore := strings.LastIndex(typeWithNumber, "_")

			if underscore != -1 {
				lunObject.Backstore = typeWithNumber[:underscore]
				lunObject.TypeNumber = typeWithNumber[underscore+1:]
			}

			lunObject.ObjectName = objectName
			return lunObject, nil
		}
	}
	return lunObject, errors.New("iscsi: getLunLinkTarget: Lun Link does not exist")
}

// ReadWriteOPS read and return the stat of read and write in megabytes,
// and total commands that send to the target
func ReadWriteOPS(iqnPath string, tpgt string, lun string) (readmb uint64,
	writemb uint64, iops uint64, err error) {

	readmbPath := filepath.Join(iqnPath, tpgt, "lun", lun,
		"statistics/scsi_tgt_port/read_mbytes")
	readmb, err = util.ReadUintFromFile(readmbPath)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("iscsi: ReadWriteOPS: read_mbytes error file %q: %w", readmbPath, err)
	}

	writembPath := filepath.Join(iqnPath, tpgt, "lun", lun,
		"statistics/scsi_tgt_port/write_mbytes")
	writemb, err = util.ReadUintFromFile(writembPath)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("iscsi: ReadWriteOPS: write_mbytes error file %q: %w", writembPath, err)
	}

	iopsPath := filepath.Join(iqnPath, tpgt, "lun", lun,
		"statistics/scsi_tgt_port/in_cmds")
	iops, err = util.ReadUintFromFile(iopsPath)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("iscsi: ReadWriteOPS: in_cmds error file %q: %w", iopsPath, err)
	}

	return readmb, writemb, iops, nil
}

// GetFileioUdev is getting the actual info to build up
// the FILEIO data and match with the enable target
func (fs FS) GetFileioUdev(fileioNumber string, objectName string) (*FILEIO, error) {
	fileio := FILEIO{
		Name:       "fileio_" + fileioNumber,
		Fnumber:    fileioNumber,
		ObjectName: objectName,
	}
	udevPath := fs.configfs.Path(targetCore, fileio.Name, fileio.ObjectName, "udev_path")

	if _, err := os.Stat(udevPath); os.IsNotExist(err) {
		return nil, fmt.Errorf("iscsi: GetFileioUdev: fileio_%s is missing file name", fileio.Fnumber)
	}
	filename, err := ioutil.ReadFile(udevPath)
	if err != nil {
		return nil, fmt.Errorf("iscsi: GetFileioUdev: Cannot read filename from udev link %q", udevPath)
	}
	fileio.Filename = strings.TrimSpace(string(filename))

	return &fileio, nil
}

// GetIblockUdev is getting the actual info to build up
// the IBLOCK data and match with the enable target
func (fs FS) GetIblockUdev(iblockNumber string, objectName string) (*IBLOCK, error) {
	iblock := IBLOCK{
		Name:       "iblock_" + iblockNumber,
		Bnumber:    iblockNumber,
		ObjectName: objectName,
	}
	udevPath := fs.configfs.Path(targetCore, iblock.Name, iblock.ObjectName, "udev_path")

	if _, err := os.Stat(udevPath); os.IsNotExist(err) {
		return nil, fmt.Errorf("iscsi: GetIBlockUdev: iblock_%s is missing file name", iblock.Bnumber)
	}
	filename, err := ioutil.ReadFile(udevPath)
	if err != nil {
		return nil, fmt.Errorf("iscsi: GetIBlockUdev: Cannot read iblock from udev link %q", udevPath)
	}
	iblock.Iblock = strings.TrimSpace(string(filename))

	return &iblock, nil
}

// GetRBDMatch is getting the actual info to build up
// the RBD data and match with the enable target
func (fs FS) GetRBDMatch(rbdNumber string, poolImage string) (*RBD, error) {
	rbd := RBD{
		Name:    "rbd_" + rbdNumber,
		Rnumber: rbdNumber,
	}
	systemRbds, err := filepath.Glob(fs.sysfs.Path(devicePath, "[0-9]*"))
	if err != nil {
		return nil, fmt.Errorf("iscsi: GetRBDMatch: Cannot find any rbd block")
	}

	for systemRbdNumber, systemRbdPath := range systemRbds {
		var systemPool, systemImage string
		systemPoolPath := filepath.Join(systemRbdPath, "pool")
		if _, err := os.Stat(systemPoolPath); os.IsNotExist(err) {
			continue
		}
		bSystemPool, err := ioutil.ReadFile(systemPoolPath)
		if err != nil {
			continue
		} else {
			systemPool = strings.TrimSpace(string(bSystemPool))
		}

		systemImagePath := filepath.Join(systemRbdPath, "name")
		if _, err := os.Stat(systemImagePath); os.IsNotExist(err) {
			continue
		}
		bSystemImage, err := ioutil.ReadFile(systemImagePath)
		if err != nil {
			continue
		} else {
			systemImage = strings.TrimSpace(string(bSystemImage))
		}

		if strings.Compare(strconv.FormatInt(int64(systemRbdNumber), 10), rbdNumber) == 0 &&
			matchPoolImage(systemPool, systemImage, poolImage) {
			rbd.Pool = systemPool
			rbd.Image = systemImage
			return &rbd, nil
		}
	}
	return nil, nil
}

// GetRDMCPPath is getting the actual info to build up RDMCP data
func (fs FS) GetRDMCPPath(rdmcpNumber string, objectName string) (*RDMCP, error) {
	rdmcp := RDMCP{
		Name:       "rd_mcp_" + rdmcpNumber,
		ObjectName: objectName,
	}
	rdmcpPath := fs.configfs.Path(targetCore, rdmcp.Name, rdmcp.ObjectName)

	if _, err := os.Stat(rdmcpPath); os.IsNotExist(err) {
		return nil, fmt.Errorf("iscsi: GetRDMCPPath %q does not exist", rdmcpPath)
	}
	isEnable, err := isPathEnable(rdmcpPath)
	if err != nil {
		return nil, fmt.Errorf("iscsi: GetRDMCPPath: error %w", err)
	}
	if isEnable {
		return &rdmcp, nil
	}
	return nil, nil
}

func matchPoolImage(pool string, image string, matchPoolImage string) (isEqual bool) {
	var poolImage = fmt.Sprintf("%s-%s", pool, image)
	return strings.Compare(poolImage, matchPoolImage) == 0
}
