package apcssh

import (
	"bufio"
	"errors"
	"fmt"
	"strings"
	"time"

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

// Abort shell connection if UPS doesn't send a recognizable response within
// the specified timeouts; Cmd timeout is very long as it is unlikely to be
// needed but still exists to avoid an indefinite hang in the unlikely event
// something does go wrong at that part of the app
const (
	shellTimeoutLogin = 20 * time.Second
	shellTimeoutCmd   = 5 * time.Minute
)

// upsCmdResult is a structure that holds all of a shell commands results
type upsCmdResult struct {
	command    string
	code       string
	codeText   string
	resultText string
}

// cmd creates an interactive shell and executes the specified command
func (cli *Client) cmd(command string) (*upsCmdResult, error) {
	// connect
	sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg)
	if err != nil {
		return nil, fmt.Errorf("failed to dial client (%w)", err)
	}
	defer sshClient.Close()

	session, err := sshClient.NewSession()
	if err != nil {
		return nil, fmt.Errorf("failed to create session (%w)", err)
	}
	defer session.Close()

	// pipes to send shell command to; and to receive repsonse
	sshInput, err := session.StdinPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to make shell input pipe (%w)", err)
	}
	sshOutput, err := session.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to make shell output pipe (%w)", err)
	}

	// make scanner to read shell output continuously
	scanner := bufio.NewScanner(sshOutput)
	scanner.Split(scanAPCShell)

	// start interactive shell
	if err := session.Shell(); err != nil {
		return nil, fmt.Errorf("failed to start shell (%w)", err)
	}

	// use a timer to close the session early in case Scan() hangs (which can
	// happen if the UPS provides output this app does not understand)
	cancelAbort := make(chan struct{})
	defer close(cancelAbort)
	go func() {
		select {
		case <-time.After(shellTimeoutLogin):
			_ = session.Close()

		case <-cancelAbort:
			// aborted cancel (i.e., succesful Scan())
		}
	}()

	// check shell response after connect
	scannedOk := scanner.Scan()
	// if failed to scan (e.g., timer closed the session after timeout)
	if !scannedOk {
		return nil, errors.New("shell did not return parsable login response")
	}
	// success; cancel abort timer
	cancelAbort <- struct{}{}
	// discard the initial shell response (login message(s) / initial shell prompt)
	_ = scanner.Bytes()

	// send command
	_, err = fmt.Fprint(sshInput, command+"\n")
	if err != nil {
		return nil, fmt.Errorf("failed to send shell command (%w)", err)
	}

	// use a timer to close the session early in case Scan() hangs (which can
	// happen if the UPS provides output this app does not understand);
	// since initial login message Scan() was okay, it is relatively unlikely this
	// will hang
	go func() {
		select {
		case <-time.After(shellTimeoutCmd):
			_ = session.Close()

		case <-cancelAbort:
			// aborted cancel (i.e., succesful Scan())
		}
	}()

	// check shell response to command
	scannedOk = scanner.Scan()
	// if failed to scan (e.g., timer closed the session after timeout)
	if !scannedOk {
		return nil, fmt.Errorf("shell did not return parsable response to cmd '%s'", command)
	}
	// success; cancel abort timer
	cancelAbort <- struct{}{}

	// parse the UPS response into result struct and return
	upsRawResponse := string(scanner.Bytes())
	result := &upsCmdResult{}

	cmdIndx := strings.Index(upsRawResponse, "\n")
	result.command = upsRawResponse[:cmdIndx-1]
	upsRawResponse = upsRawResponse[cmdIndx+1:]

	codeIndx := strings.Index(upsRawResponse, ": ")
	result.code = upsRawResponse[:codeIndx]
	upsRawResponse = upsRawResponse[codeIndx+2:]

	codeTxtIndx := strings.Index(upsRawResponse, "\n")
	result.codeText = upsRawResponse[:codeTxtIndx-1]

	// avoid out of bounds if no result text
	if codeTxtIndx+1 <= len(upsRawResponse)-2 {
		result.resultText = upsRawResponse[codeTxtIndx+1 : len(upsRawResponse)-2]
	}

	return result, nil
}