package utils

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/beclab/Olares/cli/pkg/core/logger"
	"github.com/pkg/errors"
	"github.com/saintfish/chardet"
	utilexec "k8s.io/utils/exec"
)

type Charset string

const (
	DEFAULT Charset = "DEFAULT"
	GBK     Charset = "GBK"
	UTF8    Charset = "UTF8"
	UTF16   Charset = "UTF16"
)

type CommandExecute interface {
	Run() (string, error)
	Exec() (string, error)
}

type CommandExecutor struct {
	name        string
	prefix      string
	cmd         []string
	exitCode    int
	printOutput bool
	printLine   bool
}

type PowerShellCommandExecutor struct {
	Commands    []string
	PrintOutput bool
	PrintLine   bool
}

func (p *PowerShellCommandExecutor) Run() (string, error) {
	var cmd = &CommandExecutor{
		name:        "powershell",
		prefix:      "-Command",
		cmd:         p.Commands,
		printOutput: p.PrintOutput,
		printLine:   p.PrintLine,
	}

	return cmd.run()
}

type DefaultCommandExecutor struct {
	Commands    []string
	PrintOutput bool
	PrintLine   bool
}

func (d *DefaultCommandExecutor) Run() (string, error) {
	var cmd = &CommandExecutor{
		name:        "cmd",
		prefix:      "/C",
		cmd:         d.Commands,
		printOutput: d.PrintOutput,
		printLine:   d.PrintLine,
	}

	return cmd.run()
}

func (d *DefaultCommandExecutor) RunCmd(name string, charset Charset) (string, error) {
	var cmd = &CommandExecutor{
		name:        name,
		cmd:         d.Commands,
		printOutput: d.PrintOutput,
		printLine:   d.PrintLine,
	}

	return cmd.runcmd(charset)
}

func (d *DefaultCommandExecutor) Exec() (string, error) {
	var cmd = &CommandExecutor{
		name:        "cmd",
		prefix:      "/C",
		cmd:         d.Commands,
		printOutput: d.PrintOutput,
		printLine:   d.PrintLine,
	}

	return cmd.exec()
}

func NewCommandExecutor(name, prefix string, args []string, printOutput, printLine bool) *CommandExecutor {
	return &CommandExecutor{
		name:        name,
		prefix:      prefix,
		cmd:         args,
		printOutput: printOutput,
		printLine:   printLine,
	}
}

func (command *CommandExecutor) getCmd() string {
	return strings.Join(command.cmd, " ")
}

func (command *CommandExecutor) runcmd(charset Charset) (string, error) {
	var res string
	var exec = utilexec.New()

	output, err := exec.Command(command.name, command.cmd...).Output()
	if command.printOutput {
		logger.Infof("[exec] CMD: %s, output: %s, err: %v", fmt.Sprintf("%s %v", command.name, command.cmd), string(output), err)
	}

	detector := chardet.NewTextDetector()
	result, _ := detector.DetectBest(output)
	res, _ = CharsetConverts(result.Charset, output, charset)

	if err != nil {
		logger.Debugf("[exec] CMD: %s, CHARSET: %s, OUTPUT: %s, error: %v", fmt.Sprintf("%s %v", command.name, command.cmd), result.Charset, res, err)
		return res, err
	}

	logger.Debugf("[exec] CMD: %s, CHARSET: %s, OUTPUT: %s", fmt.Sprintf("%s %v", command.name, command.cmd), result.Charset, res)
	return res, nil
}

func (command *CommandExecutor) run() (string, error) {
	args := append([]string{command.prefix}, command.cmd...)
	c := exec.Command(command.name, args...)

	out, err := c.StdoutPipe()
	if err != nil {
		return "", err
	}

	c.Stderr = c.Stdout

	if err := c.Start(); err != nil {
		command.exitCode = -1
		if exitErr, ok := err.(*exec.ExitError); ok {
			command.exitCode = exitErr.ExitCode()
		}
		return "", err
	}

	var outputBuffer bytes.Buffer
	r := bufio.NewReader(out)

	for {
		line, err := r.ReadString('\n')
		line = strings.TrimSpace(line) + "\r"
		if err != nil {
			if err.Error() != "EOF" {
				logger.Errorf("[exec] read error: %s", err)
			}

			if command.printLine && line != "" {
				fmt.Println(line)
			}
			outputBuffer.WriteString(line)
			break
		}

		if command.printLine && line != "" {
			fmt.Println(line)
		}
		outputBuffer.WriteString(line)
	}

	err = c.Wait()
	if err != nil {
		command.exitCode = -1
		if exitErr, ok := err.(*exec.ExitError); ok {
			command.exitCode = exitErr.ExitCode()
		}
	}
	res := outputBuffer.String()

	if command.printOutput {
		fmt.Printf("[exec] CMD: %s, OUTPUT: \n%s\n", c.String(), res)
	}
	logger.Debugf("[exec] CMD: %s, OUTPUT: %s", c.String(), res)
	return res, errors.Wrapf(err, "Failed to exec command: %s \n%s", command.getCmd(), res)
}

func (command *CommandExecutor) exec() (string, error) {
	args := append([]string{command.prefix}, command.cmd...)
	c := exec.Command(command.name, args...)

	out, err := c.StdoutPipe()
	if err != nil {
		return "", err
	}

	_, pipeWriter, err := os.Pipe()
	defer pipeWriter.Close()

	c.Stdin = os.Stdin
	c.Stdout = os.Stdout
	c.Stderr = c.Stdout

	if err := c.Start(); err != nil {
		command.exitCode = -1
		if exitErr, ok := err.(*exec.ExitError); ok {
			command.exitCode = exitErr.ExitCode()
		}
		return "", err
	}

	var outputBuffer bytes.Buffer
	r := bufio.NewReader(out)

	for {
		line, err := r.ReadString('\n')
		line = strings.TrimSpace(line) + "\r"
		if err != nil {
			if err.Error() != "EOF" {
				logger.Errorf("[exec] read error: %s", err)
			}

			if line != "\r" {
				_, err = pipeWriter.Write([]byte(line))
				pipeWriter.Close()
				if err != nil {
					break
				}
			}

			if command.printLine && line != "" {
				fmt.Println(line)
			}
			outputBuffer.WriteString(line)
			break
		}

		if line != "\n" && !strings.Contains(line, "\r") {
			_, err = pipeWriter.Write([]byte(line))
			if err != nil {
				break
			}
		}

		if command.printLine && line != "" {
			fmt.Println(line)
		}
		outputBuffer.WriteString(line)
	}

	err = c.Wait()
	if err != nil {
		command.exitCode = -1
		if exitErr, ok := err.(*exec.ExitError); ok {
			command.exitCode = exitErr.ExitCode()
		}
	}
	res := outputBuffer.String()

	if command.printOutput {
		fmt.Printf("[exec] CMD: %s, OUTPUT: \n%s\n", c.String(), res)
	}
	logger.Debugf("[exec] CMD: %s, OUTPUT: %s", c.String(), res)
	return res, errors.Wrapf(err, "Failed to exec command: %s \n%s", command.getCmd(), res)
}
