package winrm

import (
	"bytes"
	"crypto/x509"
	"fmt"
	"io"
	"sync"

	"github.com/masterzen/winrm/soap"
)

// Client struct
type Client struct {
	Parameters
	username string
	password string
	useHTTPS bool
	url      string
	http     Transporter
}

// Transporter does different transporters
// and init a Post request based on them
type Transporter interface {
	// init request baset on the transport configurations
	Post(*Client, *soap.SoapMessage) (string, error)
	Transport(*Endpoint) error
}

// NewClient will create a new remote client on url, connecting with user and password
// This function doesn't connect (connection happens only when CreateShell is called)
func NewClient(endpoint *Endpoint, user, password string) (*Client, error) {
	return NewClientWithParameters(endpoint, user, password, DefaultParameters)
}

// NewClientWithParameters will create a new remote client on url, connecting with user and password
// This function doesn't connect (connection happens only when CreateShell is called)
func NewClientWithParameters(endpoint *Endpoint, user, password string, params *Parameters) (*Client, error) {

	// alloc a new client
	client := &Client{
		Parameters: *params,
		username:   user,
		password:   password,
		url:        endpoint.url(),
		useHTTPS:   endpoint.HTTPS,
		// default transport
		http: &clientRequest{dial: params.Dial},
	}

	// switch to other transport if provided
	if params.TransportDecorator != nil {
		client.http = params.TransportDecorator()
	}

	// set the transport to some endpoint configuration
	if err := client.http.Transport(endpoint); err != nil {
		return nil, fmt.Errorf("Can't parse this key and certs: %s", err)
	}

	return client, nil
}

func readCACerts(certs []byte) (*x509.CertPool, error) {
	certPool := x509.NewCertPool()

	if !certPool.AppendCertsFromPEM(certs) {
		return nil, fmt.Errorf("Unable to read certificates")
	}

	return certPool, nil
}

// CreateShell will create a WinRM Shell,
// which is the prealable for running commands.
func (c *Client) CreateShell() (*Shell, error) {
	request := NewOpenShellRequest(c.url, &c.Parameters)
	defer request.Free()

	response, err := c.sendRequest(request)
	if err != nil {
		return nil, err
	}

	shellID, err := ParseOpenShellResponse(response)
	if err != nil {
		return nil, err
	}

	return c.NewShell(shellID), nil

}

// NewShell will create a new WinRM Shell for the given shellID
func (c *Client) NewShell(id string) *Shell {
	return &Shell{client: c, id: id}
}

// sendRequest exec the custom http func from the client
func (c *Client) sendRequest(request *soap.SoapMessage) (string, error) {
	return c.http.Post(c, request)
}

// Run will run command on the the remote host, writing the process stdout and stderr to
// the given writers. Note with this method it isn't possible to inject stdin.
func (c *Client) Run(command string, stdout io.Writer, stderr io.Writer) (int, error) {
	shell, err := c.CreateShell()
	if err != nil {
		return 1, err
	}
	defer shell.Close()
	cmd, err := shell.Execute(command)
	if err != nil {
		return 1, err
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		io.Copy(stdout, cmd.Stdout)
	}()

	go func() {
		defer wg.Done()
		io.Copy(stderr, cmd.Stderr)
	}()

	cmd.Wait()
	wg.Wait()
	cmd.Close()

	return cmd.ExitCode(), cmd.err
}

// RunWithString will run command on the the remote host, returning the process stdout and stderr
// as strings, and using the input stdin string as the process input
func (c *Client) RunWithString(command string, stdin string) (string, string, int, error) {
	shell, err := c.CreateShell()
	if err != nil {
		return "", "", 1, err
	}
	defer shell.Close()

	cmd, err := shell.Execute(command)
	if err != nil {
		return "", "", 1, err
	}

	if len(stdin) > 0 {
		defer cmd.Stdin.Close()
		_, err := cmd.Stdin.Write([]byte(stdin))
		if err != nil {
			return "", "", -1, err
		}
	}

	var outWriter, errWriter bytes.Buffer
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		io.Copy(&outWriter, cmd.Stdout)
	}()

	go func() {
		defer wg.Done()
		io.Copy(&errWriter, cmd.Stderr)
	}()

	cmd.Wait()
	wg.Wait()
	cmd.Close()

	return outWriter.String(), errWriter.String(), cmd.ExitCode(), cmd.err
}

// RunWithInput will run command on the the remote host, writing the process stdout and stderr to
// the given writers, and injecting the process stdin with the stdin reader.
// Warning stdin (not stdout/stderr) are bufferized, which means reading only one byte in stdin will
// send a winrm http packet to the remote host. If stdin is a pipe, it might be better for
// performance reasons to buffer it.
func (c Client) RunWithInput(command string, stdout, stderr io.Writer, stdin io.Reader) (int, error) {
	shell, err := c.CreateShell()
	if err != nil {
		return 1, err
	}
	defer shell.Close()
	cmd, err := shell.Execute(command)
	if err != nil {
		return 1, err
	}

	var wg sync.WaitGroup
	wg.Add(3)

	go func() {
		defer func() {
			cmd.Stdin.Close()
			wg.Done()
		}()
		io.Copy(cmd.Stdin, stdin)
	}()
	go func() {
		defer wg.Done()
		io.Copy(stdout, cmd.Stdout)
	}()
	go func() {
		defer wg.Done()
		io.Copy(stderr, cmd.Stderr)
	}()

	cmd.Wait()
	wg.Wait()
	cmd.Close()

	return cmd.ExitCode(), cmd.err

}
