// Copyright 2021 Chaos Mesh 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package core

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/chaos-mesh/chaosd/pkg/utils"
)

const (
	DiskFillAction         = "fill"
	DiskWritePayloadAction = "write-payload"
	DiskReadPayloadAction  = "read-payload"
)

var _ AttackConfig = &DiskAttackConfig{}

type DiskAttackConfig struct {
	CommonAttackConfig
	DdOptions       *[]DdOption
	FAllocateOption *FAllocateOption
	Path            string
}

func (d DiskAttackConfig) RecoverData() string {
	data, _ := json.Marshal(d)

	return string(data)
}

var DdCommand = utils.Command{Name: "dd"}

type DdOption struct {
	ReadPath  string `dd:"if"`
	WritePath string `dd:"of"`
	BlockSize string `dd:"bs"`
	Count     string `dd:"count"`
	Iflag     string `dd:"iflag"`
	Oflag     string `dd:"oflag"`
	Conv      string `dd:"conv"`
}

var FAllocateCommand = utils.Command{Name: "fallocate"}

type FAllocateOption struct {
	LengthOpt string `fallocate:"-"`
	Length    string `fallocate:"-"`
	FileName  string `fallocate:"-"`
}

type DiskOption struct {
	CommonAttackConfig

	Size              string `json:"size,omitempty"`
	Path              string `json:"path,omitempty"`
	Percent           string `json:"percent,omitempty"`
	PayloadProcessNum uint8  `json:"payload-process-num,omitempty"`

	FillByFallocate bool `json:"fallocate,omitempty"`
}

func NewDiskOption() *DiskOption {
	return &DiskOption{
		CommonAttackConfig: CommonAttackConfig{
			Kind: DiskAttack,
		},
		PayloadProcessNum: 1,
		FillByFallocate:   true,
	}
}

func NewDiskOptionForServer() *DiskOption {
	return &DiskOption{
		CommonAttackConfig: CommonAttackConfig{
			Kind: DiskServerAttack,
		},
		PayloadProcessNum: 1,
		FillByFallocate:   true,
	}
}

func (opt *DiskOption) PreProcess() (*DiskAttackConfig, error) {
	if err := opt.CommonAttackConfig.Validate(); err != nil {
		return nil, err
	}

	path, err := initPath(opt)
	if err != nil {
		return nil, err
	}

	byteSize, err := initSize(opt)
	if err != nil {
		return nil, err
	}

	if opt.Action == DiskFillAction && opt.FillByFallocate && byteSize != 0 {
		return &DiskAttackConfig{
			CommonAttackConfig: opt.CommonAttackConfig,
			DdOptions:          nil,
			FAllocateOption: &FAllocateOption{
				LengthOpt: "-l",
				Length:    strconv.FormatUint(byteSize, 10),
				FileName:  path,
			},
			Path: path,
		}, nil
	}

	ddOptions, err := initDdOptions(opt, path, byteSize)
	if err != nil {
		return nil, err
	}
	return &DiskAttackConfig{
		CommonAttackConfig: opt.CommonAttackConfig,
		DdOptions:          &ddOptions,
		FAllocateOption:    nil,
		Path:               path,
	}, nil
}

func initDdOptions(opt *DiskOption, path string, byteSize uint64) ([]DdOption, error) {
	ddBlocks, err := utils.SplitBytesByProcessNum(byteSize, opt.PayloadProcessNum)
	if err != nil {
		log.Error("fail to split disk size", zap.Error(err))
		return nil, err
	}
	var ddOpts []DdOption
	switch opt.Action {
	case DiskFillAction:
		for _, block := range ddBlocks {
			ddOpts = append(ddOpts, DdOption{
				ReadPath:  "/dev/zero",
				WritePath: path,
				BlockSize: block.BlockSize,
				Count:     block.Count,
				Iflag:     "fullblock", // fullblock : accumulate full blocks of input.
				Oflag:     "append",
				Conv:      "notrunc", // notrunc : do not truncate the output file.
			})
		}
	case DiskWritePayloadAction:
		for _, block := range ddBlocks {
			ddOpts = append(ddOpts, DdOption{
				ReadPath:  "/dev/zero",
				WritePath: path,
				BlockSize: block.BlockSize,
				Count:     block.Count,
				Oflag:     "dsync", // dsync : use synchronized I/O for data.
			})
		}
	case DiskReadPayloadAction:
		for _, block := range ddBlocks {
			ddOpts = append(ddOpts, DdOption{
				ReadPath:  path,
				WritePath: "/dev/null",
				BlockSize: block.BlockSize,
				Count:     block.Count,
				Iflag:     "dsync,fullblock,nocache", // nocache : Request to drop cache.
			})
		}
	}
	return ddOpts, nil
}

func initPath(opt *DiskOption) (string, error) {
	switch opt.Action {
	case DiskFillAction, DiskWritePayloadAction:
		if opt.Path == "" {
			var err error
			opt.Path, err = os.Getwd()
			if err != nil {
				log.Error("unexpected err when execute os.Getwd()", zap.Error(err))
				return "", err
			}
		}

		fi, err := os.Stat(opt.Path)
		if err != nil {
			// check if Path of file is valid when Path is not empty
			if os.IsNotExist(err) {
				var b []byte
				if err := os.WriteFile(opt.Path, b, 0600); err != nil {
					return "", err
				}
				if err := os.Remove(opt.Path); err != nil {
					return "", err
				}
				return opt.Path, nil
			}
			return "", err
		}
		if fi.IsDir() {
			opt.Path, err = utils.CreateTempFile(opt.Path)
			if err != nil {
				log.Error(fmt.Sprintf("unexpected err : %v , when CreateTempFile in action %s with path %s.", err, opt.Action, opt.Path))
				return "", err
			}
			if err := os.Remove(opt.Path); err != nil {
				return "", err
			}
			return opt.Path, err
		}
		return "", fmt.Errorf("fill into an existing file")
	case DiskReadPayloadAction:
		if opt.Path == "" {
			path, err := utils.GetRootDevice()
			if err != nil {
				log.Error("err when GetRootDevice in reading payload", zap.Error(err))
				return "", err
			}
			if path == "" {
				err = fmt.Errorf("can not get root device path")
				log.Error(fmt.Sprintf("payload action: %s", opt.Action), zap.Error(err))
				return "", err
			}
			return path, nil
		}
		var fi os.FileInfo
		var err error
		if fi, err = os.Stat(opt.Path); err != nil {
			return "", err
		}
		if fi.IsDir() {
			return "", fmt.Errorf("path is a dictory, path : %s", opt.Path)
		}
		f, err := os.Open(opt.Path)
		if err != nil {
			return "", err
		}
		err = f.Close()
		if err != nil {
			return "", nil
		}
		return opt.Path, nil
	default:
		return "", fmt.Errorf("unsupported action %s", opt.Action)
	}
}

func initSize(opt *DiskOption) (uint64, error) {
	if opt.Size != "" {
		byteSize, err := utils.ParseUnit(opt.Size)
		if err != nil {
			log.Error(fmt.Sprintf("fail to get parse size per units , %s", opt.Size), zap.Error(err))
			return 0, err
		}
		return byteSize, nil
	} else if opt.Percent != "" {
		opt.Percent = strings.Trim(opt.Percent, " %")
		percent, err := strconv.ParseUint(opt.Percent, 10, 0)
		if err != nil {
			log.Error(fmt.Sprintf("unexcepted err when parsing disk percent '%s'", opt.Percent), zap.Error(err))
			return 0, err
		}
		dir := filepath.Dir(opt.Path)
		totalSize, err := utils.GetDiskTotalSize(dir)
		if err != nil {
			log.Error("fail to get disk total size", zap.Error(err))
			return 0, err
		}
		return totalSize * percent / 100, nil
	}
	if opt.Action == DiskFillAction {
		return 0, fmt.Errorf("one of percent and size must not be empty, DiskOption : %v", opt)
	}
	return 0, fmt.Errorf("size must not be empty, DiskOption : %v", opt)
}
