package git

/*
#include <git2.h>

#include <git2/sys/credential.h>
*/
import "C"
import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/url"
	"runtime"
	"strings"
	"unsafe"

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

// RegisterManagedSSHTransport registers a Go-native implementation of an SSH
// transport that doesn't rely on any system libraries (e.g. libssh2).
//
// If Shutdown or ReInit are called, make sure that the smart transports are
// freed before it.
func RegisterManagedSSHTransport(protocol string) (*RegisteredSmartTransport, error) {
	return NewRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory)
}

func registerManagedSSH() error {
	globalRegisteredSmartTransports.Lock()
	defer globalRegisteredSmartTransports.Unlock()

	for _, protocol := range []string{"ssh", "ssh+git", "git+ssh"} {
		if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok {
			continue
		}
		managed, err := newRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory, true)
		if err != nil {
			return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
		}
		globalRegisteredSmartTransports.transports[protocol] = managed
	}
	return nil
}

func sshSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) {
	return &sshSmartSubtransport{
		transport: transport,
	}, nil
}

type sshSmartSubtransport struct {
	transport *Transport

	lastAction    SmartServiceAction
	client        *ssh.Client
	session       *ssh.Session
	stdin         io.WriteCloser
	stdout        io.Reader
	currentStream *sshSmartSubtransportStream
}

func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceAction) (SmartSubtransportStream, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	u, err := url.Parse(urlString)
	if err != nil {
		return nil, err
	}

	// Escape \ and '.
	uPath := strings.Replace(u.Path, `\`, `\\`, -1)
	uPath = strings.Replace(uPath, `'`, `\'`, -1)

	// TODO: Add percentage decode similar to libgit2.
	// Refer: https://github.com/libgit2/libgit2/blob/358a60e1b46000ea99ef10b4dd709e92f75ff74b/src/str.c#L455-L481

	var cmd string
	switch action {
	case SmartServiceActionUploadpackLs, SmartServiceActionUploadpack:
		if t.currentStream != nil {
			if t.lastAction == SmartServiceActionUploadpackLs {
				return t.currentStream, nil
			}
			t.Close()
		}
		cmd = fmt.Sprintf("git-upload-pack '%s'", uPath)

	case SmartServiceActionReceivepackLs, SmartServiceActionReceivepack:
		if t.currentStream != nil {
			if t.lastAction == SmartServiceActionReceivepackLs {
				return t.currentStream, nil
			}
			t.Close()
		}
		cmd = fmt.Sprintf("git-receive-pack '%s'", uPath)

	default:
		return nil, fmt.Errorf("unexpected action: %v", action)
	}

	cred, err := t.transport.SmartCredentials("", CredentialTypeSSHKey|CredentialTypeSSHMemory)
	if err != nil {
		return nil, err
	}
	defer cred.Free()

	sshConfig, err := getSSHConfigFromCredential(cred)
	if err != nil {
		return nil, err
	}
	sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
		marshaledKey := key.Marshal()
		cert := &Certificate{
			Kind: CertificateHostkey,
			Hostkey: HostkeyCertificate{
				Kind:         HostkeySHA1 | HostkeyMD5 | HostkeySHA256 | HostkeyRaw,
				HashMD5:      md5.Sum(marshaledKey),
				HashSHA1:     sha1.Sum(marshaledKey),
				HashSHA256:   sha256.Sum256(marshaledKey),
				Hostkey:      marshaledKey,
				SSHPublicKey: key,
			},
		}

		return t.transport.SmartCertificateCheck(cert, true, hostname)
	}

	var addr string
	if u.Port() != "" {
		addr = fmt.Sprintf("%s:%s", u.Hostname(), u.Port())
	} else {
		addr = fmt.Sprintf("%s:22", u.Hostname())
	}

	t.client, err = ssh.Dial("tcp", addr, sshConfig)
	if err != nil {
		return nil, err
	}

	t.session, err = t.client.NewSession()
	if err != nil {
		return nil, err
	}

	t.stdin, err = t.session.StdinPipe()
	if err != nil {
		return nil, err
	}

	t.stdout, err = t.session.StdoutPipe()
	if err != nil {
		return nil, err
	}

	if err := t.session.Start(cmd); err != nil {
		return nil, err
	}

	t.lastAction = action
	t.currentStream = &sshSmartSubtransportStream{
		owner: t,
	}

	return t.currentStream, nil
}

func (t *sshSmartSubtransport) Close() error {
	t.currentStream = nil
	if t.client != nil {
		t.stdin.Close()
		t.session.Wait()
		t.session.Close()
		t.client = nil
	}
	return nil
}

func (t *sshSmartSubtransport) Free() {
}

type sshSmartSubtransportStream struct {
	owner *sshSmartSubtransport
}

func (stream *sshSmartSubtransportStream) Read(buf []byte) (int, error) {
	return stream.owner.stdout.Read(buf)
}

func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) {
	return stream.owner.stdin.Write(buf)
}

func (stream *sshSmartSubtransportStream) Free() {
}

func getSSHConfigFromCredential(cred *Credential) (*ssh.ClientConfig, error) {
	switch cred.Type() {
	case CredentialTypeSSHCustom:
		credSSHCustom := (*C.git_credential_ssh_custom)(unsafe.Pointer(cred.ptr))
		data, ok := pointerHandles.Get(credSSHCustom.payload).(*credentialSSHCustomData)
		if !ok {
			return nil, errors.New("unsupported custom SSH credentials")
		}
		return &ssh.ClientConfig{
			User: C.GoString(credSSHCustom.username),
			Auth: []ssh.AuthMethod{ssh.PublicKeys(data.signer)},
		}, nil
	}

	username, _, privatekey, passphrase, err := cred.GetSSHKey()
	if err != nil {
		return nil, err
	}

	var pemBytes []byte
	if cred.Type() == CredentialTypeSSHMemory {
		pemBytes = []byte(privatekey)
	} else {
		pemBytes, err = ioutil.ReadFile(privatekey)
		if err != nil {
			return nil, err
		}
	}

	var key ssh.Signer
	if passphrase != "" {
		key, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(passphrase))
		if err != nil {
			return nil, err
		}
	} else {
		key, err = ssh.ParsePrivateKey(pemBytes)
		if err != nil {
			return nil, err
		}
	}

	return &ssh.ClientConfig{
		User: username,
		Auth: []ssh.AuthMethod{ssh.PublicKeys(key)},
	}, nil
}
