// Copyright (C) 2015 Scaleway. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.md file.

// scw helpers

// Package utils contains helpers
package utils

import (
	"crypto/md5"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"time"

	"golang.org/x/crypto/ssh"

	"github.com/mattn/go-isatty"
	"github.com/moul/gotty-client"
	"github.com/scaleway/scaleway-cli/pkg/sshcommand"
	"github.com/sirupsen/logrus"
	log "github.com/sirupsen/logrus"
)

// SpawnRedirection is used to redirects the fluxes
type SpawnRedirection struct {
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

// SSHExec executes a command over SSH and redirects file-descriptors
func SSHExec(publicIPAddress, privateIPAddress, user string, port int, command []string, checkConnection bool, gateway string, enableSSHKeyForwarding bool) error {
	gatewayUser := "root"
	gatewayIPAddress := gateway
	if strings.Contains(gateway, "@") {
		parts := strings.Split(gatewayIPAddress, "@")
		if len(parts) != 2 {
			return fmt.Errorf("gateway: must be like root@IP")
		}
		gatewayUser = parts[0]
		gatewayIPAddress = parts[1]
		gateway = gatewayUser + "@" + gatewayIPAddress
	}

	if publicIPAddress == "" && gatewayIPAddress == "" {
		return errors.New("server does not have public IP")
	}
	if privateIPAddress == "" && gatewayIPAddress != "" {
		return errors.New("server does not have private IP")
	}

	if checkConnection {
		useGateway := gatewayIPAddress != ""
		if useGateway && !IsTCPPortOpen(fmt.Sprintf("%s:22", gatewayIPAddress)) {
			return errors.New("gateway is not available, try again later")
		}
		if !useGateway && !IsTCPPortOpen(fmt.Sprintf("%s:%d", publicIPAddress, port)) {
			return errors.New("server is not ready, try again later")
		}
	}

	sshCommand := NewSSHExecCmd(publicIPAddress, privateIPAddress, user, port, isatty.IsTerminal(os.Stdin.Fd()), command, gateway, enableSSHKeyForwarding)

	log.Debugf("Executing: %s", sshCommand)

	spawn := exec.Command("ssh", sshCommand.Slice()[1:]...)
	spawn.Stdout = os.Stdout
	spawn.Stdin = os.Stdin
	spawn.Stderr = os.Stderr
	return spawn.Run()
}

// NewSSHExecCmd computes execve compatible arguments to run a command via ssh
func NewSSHExecCmd(publicIPAddress, privateIPAddress, user string, port int, allocateTTY bool, command []string, gatewayIPAddress string, enableSSHKeyForwarding bool) *sshcommand.Command {
	quiet := os.Getenv("DEBUG") != "1"
	secureExec := os.Getenv("SCW_SECURE_EXEC") == "1"
	sshCommand := &sshcommand.Command{
		AllocateTTY:         allocateTTY,
		Command:             command,
		Host:                publicIPAddress,
		Quiet:               quiet,
		SkipHostKeyChecking: !secureExec,
		User:                user,
		NoEscapeCommand:     true,
		Port:                port,
		EnableSSHKeyForwarding: enableSSHKeyForwarding,
	}
	if gatewayIPAddress != "" {
		sshCommand.Host = privateIPAddress
		sshCommand.Gateway = &sshcommand.Command{
			Host:                gatewayIPAddress,
			SkipHostKeyChecking: !secureExec,
			AllocateTTY:         allocateTTY,
			Quiet:               quiet,
			User:                user,
			Port:                port,
		}
	}

	return sshCommand
}

// GeneratingAnSSHKey generates an SSH key
func GeneratingAnSSHKey(cfg SpawnRedirection, path string, name string) (string, error) {
	args := []string{
		"-t",
		"rsa",
		"-b",
		"4096",
		"-f",
		filepath.Join(path, name),
		"-N",
		"",
		"-C",
		"",
	}
	log.Infof("Executing commands %v", args)
	spawn := exec.Command("ssh-keygen", args...)
	spawn.Stdout = cfg.Stdout
	spawn.Stdin = cfg.Stdin
	spawn.Stderr = cfg.Stderr
	return args[5], spawn.Run()
}

// WaitForTCPPortOpen calls IsTCPPortOpen in a loop
func WaitForTCPPortOpen(dest string) error {
	for {
		if IsTCPPortOpen(dest) {
			break
		}
		time.Sleep(1 * time.Second)
	}
	return nil
}

// IsTCPPortOpen returns true if a TCP communication with "host:port" can be initialized
func IsTCPPortOpen(dest string) bool {
	conn, err := net.DialTimeout("tcp", dest, time.Duration(2000)*time.Millisecond)
	if err == nil {
		defer conn.Close()
	}
	return err == nil
}

// TruncIf ensures the input string does not exceed max size if cond is met
func TruncIf(str string, max int, cond bool) string {
	if cond && len(str) > max {
		return str[:max]
	}
	return str
}

// Wordify convert complex name to a single word without special shell characters
func Wordify(str string) string {
	str = regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAllString(str, "_")
	str = regexp.MustCompile(`__+`).ReplaceAllString(str, "_")
	str = strings.Trim(str, "_")
	return str
}

// PathToTARPathparts returns the two parts of a unix path
func PathToTARPathparts(fullPath string) (string, string) {
	fullPath = strings.TrimRight(fullPath, "/")
	return path.Dir(fullPath), path.Base(fullPath)
}

// RemoveDuplicates transforms an array into a unique array
func RemoveDuplicates(elements []string) []string {
	encountered := map[string]bool{}

	// Create a map of all unique elements.
	for v := range elements {
		encountered[elements[v]] = true
	}

	// Place all keys from the map into a slice.
	result := []string{}
	for key := range encountered {
		result = append(result, key)
	}
	return result
}

// AttachToSerial tries to connect to server serial using 'gotty-client' and fallback with a help message
func AttachToSerial(serverID, apiToken, url string) (*gottyclient.Client, chan bool, error) {
	gottyURL := os.Getenv("SCW_GOTTY_URL")
	if gottyURL == "" {
		gottyURL = url
	}
	URL := fmt.Sprintf("%s?arg=%s&arg=%s", gottyURL, apiToken, serverID)

	logrus.Debug("Connection to ", URL)
	gottycli, err := gottyclient.NewClient(URL)
	if err != nil {
		return nil, nil, err
	}

	if os.Getenv("SCW_TLSVERIFY") == "0" {
		gottycli.SkipTLSVerify = true
	}

	gottycli.UseProxyFromEnv = true

	if err = gottycli.Connect(); err != nil {
		return nil, nil, err
	}
	done := make(chan bool)

	fmt.Println("You are connected, type 'Ctrl+q' to quit.")
	go func() {
		gottycli.Loop()
		gottycli.Close()
		done <- true
	}()
	return gottycli, done, nil
}

func rfc4716hex(data []byte) string {
	fingerprint := ""

	for i := 0; i < len(data); i++ {
		fingerprint = fmt.Sprintf("%s%0.2x", fingerprint, data[i])
		if i != len(data)-1 {
			fingerprint = fingerprint + ":"
		}
	}
	return fingerprint
}

// SSHGetFingerprint returns the fingerprint of an SSH key
func SSHGetFingerprint(key []byte) (string, error) {
	publicKey, comment, _, _, err := ssh.ParseAuthorizedKey(key)
	if err != nil {
		return "", err
	}
	switch reflect.TypeOf(publicKey).String() {
	case "*ssh.rsaPublicKey", "*ssh.dsaPublicKey", "*ssh.ecdsaPublicKey":
		md5sum := md5.Sum(publicKey.Marshal())
		return publicKey.Type() + " " + rfc4716hex(md5sum[:]) + " " + comment, nil
	default:
		return "", errors.New("Can't handle this key")
	}
}
