package main

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"

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

type Sshd struct {
	AuthorizedKeysMap map[string]bool
	Filestore         Filestore
}

func (s *Sshd) Listen(iface string) error {
	// An SSH server is represented by a ServerConfig, which holds
	// certificate details and handles authentication of ServerConns.
	config := &ssh.ServerConfig{
		// Remove to disable public key auth.
		PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
			if s.AuthorizedKeysMap[string(pubKey.Marshal())] {
				return &ssh.Permissions{
					// Record the public key used for authentication.
					Extensions: map[string]string{
						"pubkey-fp": ssh.FingerprintSHA256(pubKey),
					},
				}, nil
			}
			return nil, fmt.Errorf("unknown public key for %q", c.User())
		},
	}

	bitSize := 4096

	privateKey, err := generatePrivateKey(bitSize)
	if err != nil {
		return err
	}

	privateBytes := encodePrivateKeyToPEM(privateKey)

	private, err := ssh.ParsePrivateKey(privateBytes)
	if err != nil {
		return fmt.Errorf("Failed to parse private key: %w", err)
	}

	config.AddHostKey(private)

	// Once a ServerConfig has been configured, connections can be
	// accepted.
	listener, err := net.Listen("tcp", iface)
	if err != nil {
		return err
	}

	return s.HandleRequests(listener, config)
}

func (s *Sshd) HandleRequests(listener net.Listener, config *ssh.ServerConfig) error {
	for {
		nConn, err := listener.Accept()
		if err != nil {
			return fmt.Errorf("failed to accept incoming connection: %w", err)
		}

		// Before use, a handshake must be performed on the incoming
		// net.Conn.
		conn, chans, reqs, err := ssh.NewServerConn(nConn, config)
		if err != nil {
			log.Printf("failed to handshake: %s", err)
			continue
		}
		log.Printf("logged in with key %s", conn.Permissions.Extensions["pubkey-fp"])

		// The incoming Request channel must be serviced.
		go ssh.DiscardRequests(reqs)

		// Service the incoming Channel channel.
		for newChannel := range chans {
			// Channels have a type, depending on the application level
			// protocol intended. In the case of a shell, the type is
			// "session" and ServerShell may be used to present a simple
			// terminal interface.
			if newChannel.ChannelType() != "session" {
				newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
				continue
			}
			channel, requests, err := newChannel.Accept()
			if err != nil {
				return fmt.Errorf("Could not accept channel: %w", err)
			}

			session := &SshSession{
				shell:     NewShell("user@host:/# "),
				filestore: s.Filestore,
			}

			// Sessions have out-of-band requests such as "shell",
			// "pty-req" and "env".  Here we handle only the
			// "shell" request.
			go session.HandleRequests(requests)

			go session.RunShell(channel)
		}
	}
}

type SshSession struct {
	shell     *Shell
	filestore Filestore
}

func (s *SshSession) HandleRequests(in <-chan *ssh.Request) {
	for req := range in {
		log.Printf("received request of type \"%s\"", req.Type)

		var ok bool
		switch req.Type {
		case "shell":
			ok = true
		case "pty-req":
			// Responding 'ok' here will let the client
			// know we have a pty ready for input
			ok = true
			// Parse body...
			termLen := req.Payload[3]
			termEnv := string(req.Payload[4 : termLen+4])
			w, h := parseDims(req.Payload[termLen+4:])
			s.shell.SetWinsize(w, h)
			log.Printf("pty-req '%s'", termEnv)
		}

		req.Reply(ok, nil)
	}
}

func (s *SshSession) RunShell(channel io.ReadWriteCloser) {
	s.shell.Attach(channel)
	handler := NewCommandHandler(s.shell, s.filestore)

	go func() {
		for {
			comm, err := s.shell.ReadCommand()
			if err != nil {
				if errors.Is(err, io.EOF) {
					log.Println("Received EOF, closing TTY")
				} else {
					log.Println("Error reading command: ", err)
				}

				if !errors.Is(err, ErrEmptyCommand) {
					s.shell.Close()
					return
				}
			} else {
				log.Printf("command: \"%s\" %s", comm.Name, comm.Arguments)

				handler.Handle(comm)
			}

			err = s.shell.WritePrompt()
			if err != nil {
				log.Println(err)
			}
		}
	}()
}
