package apcssh

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"path"

	"golang.org/x/crypto/ssh"
)

// UploadSCP uploads a file to the destination specified (e.g., "/ssl/file.key")
// containing the file content specified. An existing file at the destination
// will be overwritten without warning.
func (cli *Client) UploadSCP(destination string, fileContent []byte, filePermissions fs.FileMode) error {
	// connect
	sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to dial client (%w)", err)
	}
	defer sshClient.Close()

	// make session to use for SCP
	session, err := sshClient.NewSession()
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to create session (%w)", err)
	}
	defer session.Close()

	// attach pipes
	out, err := session.StdoutPipe()
	if err != nil {
		return err
	}
	w, err := session.StdinPipe()
	if err != nil {
		return err
	}
	defer w.Close()

	// send execute cmd --
	// build cmd to send as request
	// Go implementation sends additional 0x22 bytes when using Run() (as
	// compared to putty's scp tool). these additional bytes seem to cause the
	// apc ups to fail execution of the command
	payload := []byte(fmt.Sprintf("scp -q -t %s", destination))
	payloadLen := uint8(len(payload))
	payload = append([]byte{0, 0, 0, payloadLen}, payload...)

	ok, err := session.SendRequest("exec", true, payload)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to execute scp cmd (%w)", err)
	}
	if !ok {
		return errors.New("apcssh: scp: execute scp cmd not ok")
	}

	// check remote response
	// Note: File upload may not work if the client doesn't actually read from
	// the remote output.
	err = scpCheckResponse(out)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send scp cmd (bad remote response 1) (%w)", err)
	}

	// just file name (without path)
	filename := path.Base(destination)

	// send file header
	_, err = fmt.Fprintln(w, "C"+fmt.Sprintf("%04o", filePermissions.Perm()), len(fileContent), filename)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send file info (%w)", err)
	}

	err = scpCheckResponse(out)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send file info (bad remote response 2) (%w)", err)
	}

	// send actual file
	_, err = io.Copy(w, bytes.NewReader(fileContent))
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send file(%w)", err)
	}

	// send file end
	_, err = fmt.Fprint(w, "\x00")
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send final 00 byte (%w)", err)
	}

	err = scpCheckResponse(out)
	if err != nil {
		return fmt.Errorf("apcssh: scp: failed to send file (bad remote response 3) (%w)", err)
	}

	// done
	return nil
}

// scpCheckResponse reads the output from the remote and returns an error
// if the remote output was not 0
func scpCheckResponse(remoteOutPipe io.Reader) error {
	buffer := make([]uint8, 1)
	_, err := remoteOutPipe.Read(buffer)
	if err != nil {
		return fmt.Errorf("apcssh: failed to read output buffer (%w)", err)
	}

	responseType := buffer[0]
	message := ""
	if responseType > 0 {
		bufferedReader := bufio.NewReader(remoteOutPipe)
		message, err = bufferedReader.ReadString('\n')
		if err != nil {
			return fmt.Errorf("apcssh: failed to read output buffer (%w)", err)
		}
	}

	// if not 0 (aka OK)
	if responseType != 0 {
		return fmt.Errorf("apcssh: remote returned error (%d: %s)", responseType, message)
	}

	return nil
}