package hosts

import (
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

	"k8s.io/client-go/transport"

	v3 "github.com/rancher/rke/types"
	"golang.org/x/crypto/ssh"
)

const (
	DockerDialerTimeout = 50
)

type DialerFactory func(h *Host) (func(network, address string) (net.Conn, error), error)

type dialer struct {
	signer          ssh.Signer
	sshKeyString    string
	sshCertString   string
	sshAddress      string
	username        string
	netConn         string
	dockerSocket    string
	useSSHAgentAuth bool
	bastionDialer   *dialer
}

type DialersOptions struct {
	DockerDialerFactory    DialerFactory
	LocalConnDialerFactory DialerFactory
	K8sWrapTransport       transport.WrapperFunc
}

func GetDialerOptions(d, l DialerFactory, w transport.WrapperFunc) DialersOptions {
	return DialersOptions{
		DockerDialerFactory:    d,
		LocalConnDialerFactory: l,
		K8sWrapTransport:       w,
	}
}

func newDialer(h *Host, kind string) (*dialer, error) {
	// Check for Bastion host connection
	var bastionDialer *dialer
	if len(h.BastionHost.Address) > 0 {
		bastionDialer = &dialer{
			sshAddress:      fmt.Sprintf("%s:%s", h.BastionHost.Address, h.BastionHost.Port),
			username:        h.BastionHost.User,
			sshKeyString:    h.BastionHost.SSHKey,
			sshCertString:   h.BastionHost.SSHCert,
			netConn:         "tcp",
			useSSHAgentAuth: h.SSHAgentAuth,
		}
		if bastionDialer.sshKeyString == "" && !bastionDialer.useSSHAgentAuth {
			var err error
			bastionDialer.sshKeyString, err = privateKeyPath(h.BastionHost.SSHKeyPath)
			if err != nil {
				return nil, err
			}

			if bastionDialer.sshCertString == "" && len(h.BastionHost.SSHCertPath) > 0 {
				bastionDialer.sshCertString, err = certificatePath(h.BastionHost.SSHCertPath)
				if err != nil {
					return nil, err
				}
			}
		}
	}

	dialer := &dialer{
		sshAddress:      fmt.Sprintf("%s:%s", h.Address, h.Port),
		username:        h.User,
		dockerSocket:    h.DockerSocket,
		sshKeyString:    h.SSHKey,
		sshCertString:   h.SSHCert,
		netConn:         "unix",
		useSSHAgentAuth: h.SSHAgentAuth,
		bastionDialer:   bastionDialer,
	}

	if dialer.sshKeyString == "" && !dialer.useSSHAgentAuth {
		var err error
		dialer.sshKeyString, err = privateKeyPath(h.SSHKeyPath)
		if err != nil {
			return nil, err
		}

		if dialer.sshCertString == "" && len(h.SSHCertPath) > 0 {
			dialer.sshCertString, err = certificatePath(h.SSHCertPath)
			if err != nil {
				return nil, err
			}
		}
	}

	switch kind {
	case "network", "health":
		dialer.netConn = "tcp"
	}

	if len(dialer.dockerSocket) == 0 {
		dialer.dockerSocket = "/var/run/docker.sock"
	}

	return dialer, nil
}

func SSHFactory(h *Host) (func(network, address string) (net.Conn, error), error) {
	dialer, err := newDialer(h, "docker")
	return dialer.Dial, err
}

func LocalConnFactory(h *Host) (func(network, address string) (net.Conn, error), error) {
	dialer, err := newDialer(h, "network")
	return dialer.Dial, err
}

func (d *dialer) DialDocker(network, addr string) (net.Conn, error) {
	return d.Dial(network, addr)
}

func (d *dialer) DialLocalConn(network, addr string) (net.Conn, error) {
	return d.Dial(network, addr)
}

func (d *dialer) Dial(network, addr string) (net.Conn, error) {
	var conn *ssh.Client
	var err error
	if d.bastionDialer != nil {
		conn, err = d.getBastionHostTunnelConn()
	} else {
		conn, err = d.getSSHTunnelConnection()
	}
	if err != nil {
		if strings.Contains(err.Error(), "no key found") {
			return nil, fmt.Errorf("Unable to access node with address [%s] using SSH. Please check if the configured key or specified key file is a valid SSH Private Key. Error: %v", d.sshAddress, err)
		} else if strings.Contains(err.Error(), "no supported methods remain") {
			return nil, fmt.Errorf("Unable to access node with address [%s] using SSH. Please check if you are able to SSH to the node using the specified SSH Private Key and if you have configured the correct SSH username. Error: %v", d.sshAddress, err)
		} else if strings.Contains(err.Error(), "cannot decode encrypted private keys") {
			return nil, fmt.Errorf("Unable to access node with address [%s] using SSH. Using encrypted private keys is only supported using ssh-agent. Please configure the option `ssh_agent_auth: true` in the configuration file or use --ssh-agent-auth as a parameter when running RKE. This will use the `SSH_AUTH_SOCK` environment variable. Error: %v", d.sshAddress, err)
		} else if strings.Contains(err.Error(), "operation timed out") {
			return nil, fmt.Errorf("Unable to access node with address [%s] using SSH. Please check if the node is up and is accepting SSH connections or check network policies and firewall rules. Error: %v", d.sshAddress, err)
		}
		return nil, fmt.Errorf("Failed to dial ssh using address [%s]: %v", d.sshAddress, err)
	}

	// Docker Socket....
	if d.netConn == "unix" {
		addr = d.dockerSocket
		network = d.netConn
	}

	remote, err := conn.Dial(network, addr)
	if err != nil {
		if strings.Contains(err.Error(), "connect failed") {
			return nil, fmt.Errorf("Unable to access the service on %s. The service might be still starting up. Error: %v", addr, err)
		} else if strings.Contains(err.Error(), "administratively prohibited") {
			return nil, fmt.Errorf("Unable to access the Docker socket (%s). Please check if the configured user can execute `docker ps` on the node, and if the SSH server version is at least version 6.7 or higher. If you are using RedHat/CentOS, you can't use the user `root`. Please refer to the documentation for more instructions. Error: %v", addr, err)
		}
		return nil, fmt.Errorf("Failed to dial to %s: %v", addr, err)
	}
	return remote, err
}

func (d *dialer) getSSHTunnelConnection() (*ssh.Client, error) {
	cfg, err := getSSHConfig(d.username, d.sshKeyString, d.sshCertString, d.useSSHAgentAuth)
	if err != nil {
		return nil, fmt.Errorf("Error configuring SSH: %v", err)
	}
	// Establish connection with SSH server
	return ssh.Dial("tcp", d.sshAddress, cfg)
}

func (h *Host) newHTTPClient(dialerFactory DialerFactory) (*http.Client, error) {
	factory := dialerFactory
	if factory == nil {
		factory = SSHFactory
	}

	dialer, err := factory(h)
	if err != nil {
		return nil, err
	}
	dockerDialerTimeout := time.Second * DockerDialerTimeout
	return &http.Client{
		Transport: &http.Transport{
			Dial:                  dialer,
			TLSHandshakeTimeout:   dockerDialerTimeout,
			IdleConnTimeout:       dockerDialerTimeout,
			ResponseHeaderTimeout: dockerDialerTimeout,
		},
	}, nil
}

func (d *dialer) getBastionHostTunnelConn() (*ssh.Client, error) {
	bastionCfg, err := getSSHConfig(d.bastionDialer.username, d.bastionDialer.sshKeyString, d.bastionDialer.sshCertString, d.bastionDialer.useSSHAgentAuth)
	if err != nil {
		return nil, fmt.Errorf("Error configuring SSH for bastion host [%s]: %v", d.bastionDialer.sshAddress, err)
	}
	bastionClient, err := ssh.Dial("tcp", d.bastionDialer.sshAddress, bastionCfg)
	if err != nil {
		return nil, fmt.Errorf("Failed to connect to the bastion host [%s]: %v", d.bastionDialer.sshAddress, err)
	}
	conn, err := bastionClient.Dial(d.bastionDialer.netConn, d.sshAddress)
	if err != nil {
		return nil, fmt.Errorf("Failed to connect to the host [%s]: %v", d.sshAddress, err)
	}
	cfg, err := getSSHConfig(d.username, d.sshKeyString, d.sshCertString, d.useSSHAgentAuth)
	if err != nil {
		return nil, fmt.Errorf("Error configuring SSH for host [%s]: %v", d.sshAddress, err)
	}
	newClientConn, channels, sshRequest, err := ssh.NewClientConn(conn, d.sshAddress, cfg)
	if err != nil {
		return nil, fmt.Errorf("Failed to establish new ssh client conn [%s]: %v", d.sshAddress, err)
	}
	return ssh.NewClient(newClientConn, channels, sshRequest), nil
}

func BastionHostWrapTransport(bastionHost v3.BastionHost) (transport.WrapperFunc, error) {

	bastionDialer := &dialer{
		sshAddress:      fmt.Sprintf("%s:%s", bastionHost.Address, bastionHost.Port),
		username:        bastionHost.User,
		sshKeyString:    bastionHost.SSHKey,
		sshCertString:   bastionHost.SSHCert,
		netConn:         "tcp",
		useSSHAgentAuth: bastionHost.SSHAgentAuth,
	}

	if bastionDialer.sshKeyString == "" && !bastionDialer.useSSHAgentAuth {
		var err error
		bastionDialer.sshKeyString, err = privateKeyPath(bastionHost.SSHKeyPath)
		if err != nil {
			return nil, err
		}

	}

	if bastionDialer.sshCertString == "" && len(bastionHost.SSHCertPath) > 0 {
		var err error
		bastionDialer.sshCertString, err = certificatePath(bastionHost.SSHCertPath)
		if err != nil {
			return nil, err
		}

	}
	return func(rt http.RoundTripper) http.RoundTripper {
		if ht, ok := rt.(*http.Transport); ok {
			ht.DialContext = nil
			ht.DialTLS = nil
			ht.Dial = bastionDialer.Dial
		}
		return rt
	}, nil
}
