package host

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
)

func (t *TiUPCluster) Diag() *Diag {
	return t.diag
}

type Diag struct {
	tiupCluster *TiUPCluster

	ControlNodeHost *WorkloadHost
	ClusterName     string
	CMDTimeout      time.Duration

	UploadMaxSizeKB       int
	collectMinFreeSpaceKB int
}

func newDiag(tiupCluster *TiUPCluster) *Diag {
	return &Diag{
		tiupCluster:           tiupCluster,
		ControlNodeHost:       tiupCluster.ControlNodeHost,
		ClusterName:           tiupCluster.ClusterName,
		CMDTimeout:            tiupCluster.CMDTimeout,
		UploadMaxSizeKB:       500 * 1024,       // default to 500 MB
		collectMinFreeSpaceKB: 32 * 1024 * 1024, // disallow collect if free space < 32 GB
	}
}

// WithUploadMaxSize sets maximum size.
// Collected data won't be uploaded if exceeded the setting
//
// This is a naive way to not overwhelm our outbound bandwidth
func (d *Diag) WithUploadMaxSize(kiloBytes int) *Diag {
	d.UploadMaxSizeKB = kiloBytes
	return d
}

type DiagCollectAndUploadOptions struct {
	Collect DiagCollectOptions
	Upload  DiagUploadOptions
}

type DiagCollectOptions struct {
	Output string
	Flags  []string
}

type DiagUploadOptions struct {
	Username string
	Password string
	Flags    []string
}

func (d *Diag) CollectAndUpload(opt DiagCollectAndUploadOptions) (string, error) {
	if len(opt.Upload.Username) == 0 || len(opt.Upload.Password) == 0 {
		return "", fmt.Errorf("opt.Upload.Username and opt.Upload.Password must be provided")
	}

	collectedDir, err := d.collect(opt.Collect.Output, opt.Collect.Flags...)
	if err != nil {
		return "", errors.Trace(err)
	}

	url, err := d.upload(collectedDir, opt.Upload.Username, opt.Upload.Password, opt.Upload.Flags...)
	return url, errors.Trace(err)
}

func (d *Diag) collect(output string, flags ...string) (outputDir string, err error) {
	outputDir = output
	if len(outputDir) == 0 {
		outputDir = fmt.Sprintf("/tmp/%s.%s", time.Now().Format(time.RFC3339), "diag")
	}

	err = d.collectFreeSpaceCheck(outputDir)
	if err != nil {
		return "", errors.Trace(err)
	}

	return outputDir, d.tiupCluster.run(
		fmt.Sprintf("yes | tiup diag collect %s -o %s", d.ClusterName, outputDir), flags)
}

func (d *Diag) collectFreeSpaceCheck(output string) error {
	dfOutput, err := d.ControlNodeHost.Exec(fmt.Sprintf("df $(dirname %s) --output=avail | sed 1d", output), time.Minute)
	if err != nil {
		return errors.Trace(err)
	}
	if dfOutput.ExitCode != 0 || len(dfOutput.Stderr) > 0 {
		return fmt.Errorf("command failed, exitcode %d, stderr %s", dfOutput.ExitCode, dfOutput.Stderr)
	}

	avaliableSize, err := strconv.Atoi(dfOutput.Stdout)
	if err != nil {
		return errors.Trace(err)
	}

	if avaliableSize < d.collectMinFreeSpaceKB {
		return fmt.Errorf("diag collect disallowed, free space of %s is lower than %d kB", output, d.collectMinFreeSpaceKB)
	}

	return nil
}

func (d *Diag) upload(inputFile, username, password string, flags ...string) (string, error) {
	err := d.uploadSizeCheck(inputFile)
	if err != nil {
		return "", errors.Trace(err)
	}

	cmd := fmt.Sprintf("tiup diag upload %s -u '%s' -p '%s'", inputFile, username, password)
	cmd += " " + strings.Join(flags, " ")
	uploadOutput, err := d.ControlNodeHost.Exec(cmd, d.CMDTimeout)
	if err != nil {
		return "", errors.Trace(err)
	}
	output := uploadOutput.Stdout
	var re = regexp.MustCompile(`(?s).*Download URL: "([^\"]+).*`)
	group := re.FindStringSubmatch(output)
	if len(group) != 2 {
		log.Info(output)
		return "", fmt.Errorf("no download url in the output")
	}
	url := group[1]

	return url, nil
}

func (d *Diag) uploadSizeCheck(inputFile string) error {
	duOutput, err := d.ControlNodeHost.Exec(fmt.Sprintf("du %s --max-depth=0 | cut -f1", inputFile), time.Minute)
	if err != nil {
		return errors.Trace(err)
	}
	if duOutput.ExitCode != 0 || len(duOutput.Stderr) > 0 {
		return fmt.Errorf("command failed, exitcode %d, stderr %s", duOutput.ExitCode, duOutput.Stderr)
	}

	actualSize, err := strconv.Atoi(duOutput.Stdout)
	if err != nil {
		return errors.Trace(err)
	}

	if actualSize > d.UploadMaxSizeKB {
		return fmt.Errorf("upload size %d kB exceeds max size %d kB", actualSize, d.UploadMaxSizeKB)
	}

	return nil
}
