package service

import (
	"fmt"
	"git.wamblee.org/converge/pkg/agent/session"
	"git.wamblee.org/converge/pkg/agent/terminal"
	"git.wamblee.org/converge/pkg/comms"
	"git.wamblee.org/converge/pkg/support/ioutils"
	"github.com/gliderlabs/ssh"
	"github.com/pkg/sftp"
	"io"
	"log"
	"net"
	"os"
	"strings"
)

type SshAgentService struct {
	hostPrivateKey []byte
	shellCommand   string
	authorizedKeys *AuthorizedPublicKeys
}

func NewSshAgentService(hostPrivateKey []byte, shellCommand string,
	authorizedKeys *AuthorizedPublicKeys) SshAgentService {
	return SshAgentService{
		hostPrivateKey: hostPrivateKey,
		shellCommand:   shellCommand,
		authorizedKeys: authorizedKeys,
	}
}

func (s SshAgentService) Run(listener net.Listener) {
	s.sshServer().Serve(listener)
}

type SshAgentSession struct {
	session ssh.Session
}

func NewSshAgentSession(session ssh.Session) SshAgentSession {
	return SshAgentSession{session: session}
}

func (s SshAgentSession) MessageUser(message string) {
	for _, line := range strings.Split(message, "\n") {
		io.WriteString(s.session.Stderr(), "### "+line+"\n\r")
	}
	io.WriteString(s.session.Stderr(), "\n\r")
}

func (s SshAgentSession) Type() string {
	sessionType := s.session.Subsystem()
	if sessionType == "" {
		sessionType = "ssh"
	}
	return sessionType
}

func (s *SshAgentService) sshServer() *ssh.Server {
	ssh.Handle(func(sshSession ssh.Session) {
		workingDirectory, _ := os.Getwd()
		env := append(os.Environ(), fmt.Sprintf("agentdir=%s", workingDirectory))
		process, err := terminal.PtySpawner.Start(sshSession, env, s.shellCommand)
		if err != nil {
			panic(err)
		}
		sessionInfo := comms.NewSessionInfo(
			sshSession.LocalAddr().String(), "ssh",
		)
		session.Login(sessionInfo, NewSshAgentSession(sshSession))
		// For SSH we detect activity when there are writes to the shell that was spawned.
		// This means user input.
		activityDetector := NewWriteDetector(process.Pipe())
		ioutils.SynchronizeStreams("shell -- ssh", activityDetector, sshSession)
		session.LogOut(sessionInfo.ClientId)
		// Using Kill() here will create defunct processes and in normal
		// circumstances Wait() will be the best because the process will be shutting
		// down automatically becuase it has lost its terminal.
		process.Wait()
	})

	log.Println("starting ssh server, waiting for debug sessions")

	server := ssh.Server{
		PublicKeyHandler: s.authorizedKeys.authorize,
		SubsystemHandlers: map[string]ssh.SubsystemHandler{
			"sftp": SftpHandler,
		},
	}
	option := ssh.HostKeyPEM(s.hostPrivateKey)
	option(&server)

	return &server
}

func SftpHandler(sftpSession ssh.Session) {
	sessionInfo := comms.NewSessionInfo(
		sftpSession.LocalAddr().String(),
		"sftp",
	)
	session.Login(sessionInfo, NewSshAgentSession(sftpSession))
	defer session.LogOut(sessionInfo.ClientId)

	debugStream := io.Discard
	serverOptions := []sftp.ServerOption{
		sftp.WithDebug(debugStream),
	}
	// activity for sftp means that the server is sending data to the client.
	// In contrast to activity detection for ssh we use activity detection based on
	// serveractivity. This approach ensures that long downloads are not interrupted
	// by a timeout and are allowed to finish.
	activityDetector := NewSftpActivityDetector(sftpSession)
	server, err := sftp.NewServer(
		activityDetector,
		serverOptions...,
	)
	if err != nil {
		log.Printf("sftp tcpserver init error: %s\n", err)
		return
	}
	if err := server.Serve(); err == io.EOF {
		server.Close()
		fmt.Println("sftp client exited session.")
	} else if err != nil {
		fmt.Println("sftp tcpserver completed with error:", err)
	}
}