package command

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/HikariKnight/quickpassthrough/internal/common"
	"github.com/HikariKnight/quickpassthrough/internal/logger"
)

// Run a command and return STDOUT
func Run(binary string, args ...string) ([]string, error) {
	var stdout, stderr bytes.Buffer

	// Configure the ls-iommu c--ommand
	cmd := exec.Command(binary, args...)
	cmd.Stderr = &stderr
	cmd.Stdout = &stdout

	// Execute the command
	err := cmd.Run()

	// Read the output
	output, _ := io.ReadAll(&stdout)

	// Get the output
	outputs := make([]string, 0, 1)
	outputs = append(outputs, string(output))

	// Return our list of items
	return outputs, err
}

// RunErr is just like command.Run() but also returns STDERR
func RunErr(binary string, args ...string) ([]string, []string, error) {
	var stdout, stderr bytes.Buffer

	// Configure the ls-iommu c--ommand
	cmd := exec.Command(binary, args...)
	cmd.Stderr = &stderr
	cmd.Stdout = &stdout

	// Execute the command
	err := cmd.Run()

	// Read the output
	output, _ := io.ReadAll(&stdout)
	outerr, _ := io.ReadAll(&stderr)

	// Get the output
	var outputs, outerrs []string
	outputs = append(outputs, string(output))
	outerrs = append(outerrs, string(outerr))

	// Return our list of items
	return outputs, outerrs, err
}

func RunErrSudo(isRoot bool, binary string, args ...string) ([]string, []string, error) {
	if !isRoot && binary != "sudo" {
		args = append([]string{binary}, args...)
		binary = "sudo"
	}
	logger.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
	fmt.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
	return RunErr(binary, args...)
}

// Elevate elevates this functions runs the command "sudo -Sk -- echo",
// this forces sudo to re-authenticate and lets us enter the password to STDIN
// giving us the ability to run sudo commands
func Elevate(password string) {
	// Do a simple sudo command to just authenticate with sudo
	cmd := exec.Command("sudo", "-S", "--", "echo")

	// Wait for 500ms, if the password is correct, sudo will return immediately
	cmd.WaitDelay = 1000 * time.Millisecond

	// Open STDIN
	stdin, err := cmd.StdinPipe()
	common.ErrorCheck(err, "\nFailed to get sudo STDIN")

	// Start the authentication
	err = cmd.Start()
	common.ErrorCheck(err, "\nFailed to start sudo command")

	// Get the passed password
	pw, _ := base64.StdEncoding.DecodeString(password)
	_, err = stdin.Write([]byte(string(pw) + "\n"))
	common.ErrorCheck(err, "\nFailed at typing to STDIN")
	// Clear the password
	pw = nil
	password = ""

	_ = stdin.Close()

	// Wait for the sudo prompt (If the correct password was given, it will not stay behind)
	err = cmd.Wait()
	common.ErrorCheck(err, "\nError, password given was wrong")
}

// Clear clears the terminal.
func Clear() {
	c := exec.Command("clear")
	c.Stdout = os.Stdout
	_ = c.Run()
}

// ExecAndLogSudo executes an elevated command and logs the output.
//
// * if we're root, the command is executed directly
// * if we're not root, the command is prefixed with "sudo"
//
//   - noisy determines if we should print the command to the user
//     noisy isn't set to true by our copy caller, as it logs differently,
//     but other callers set it.
func ExecAndLogSudo(isRoot, noisy bool, exe string, args ...string) error {
	if !isRoot && exe != "sudo" {
		og := exe
		exe = "sudo"
		newArgs := make([]string, 0)
		newArgs = append(newArgs, og)
		newArgs = append(newArgs, args...)
		args = newArgs
	}

	// Write to logger
	logger.Printf("Executing (elevated): %s %s\n", exe, strings.Join(args, " "))

	if noisy {
		// Print to the user
		fmt.Printf("Executing (elevated): %s %s\nSee debug.log for detailed output\n", exe, strings.Join(args, " "))
	}

	wd, err := os.Getwd()
	if err != nil {
		return err
	}

	r := exec.Command(exe, args...)
	r.Dir = wd

	cmdCombinedOut, err := r.CombinedOutput()
	outStr := string(cmdCombinedOut)

	// Write to logger, tabulate output
	// tabulation denotes it's hierarchy as a child of the command
	outStr = strings.ReplaceAll(outStr, "\n", "\n\t")
	logger.Printf("\t" + string(cmdCombinedOut) + "\n")
	if noisy {
		// Print to the user
		fmt.Printf("%s\n", outStr)
	}

	if err != nil {
		err = fmt.Errorf("failed to execute %s: %w\n%s", exe, err, outStr)
	}

	return err
}