package ssh

import (
	"context"
	"io"
	"net"
	"os"
	"time"
	"unicode/utf8"

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

	"git.t-juice.club/torjus/apiary/config"

	"git.t-juice.club/torjus/apiary/honeypot/ssh/store"
	"git.t-juice.club/torjus/apiary/models"
	sshlib "github.com/gliderlabs/ssh"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

type HoneypotServer struct {
	Logger *zap.SugaredLogger

	sshServer *sshlib.Server

	attemptStore      store.LoginAttemptStore
	attemptsCallbacks []func(l models.LoginAttempt)

	throttleSpeed float64
}

func NewHoneypotServer(cfg config.HoneypotConfig, store store.LoginAttemptStore) (*HoneypotServer, error) {
	var hs HoneypotServer
	hs.attemptStore = store
	hs.Logger = zap.NewNop().Sugar()

	hs.sshServer = &sshlib.Server{
		Addr:            cfg.ListenAddr,
		PasswordHandler: hs.passwordHandler,
		ConnCallback:    hs.connCallback,
		Handler:         handler,
		Version:         "OpenSSH_7.4p1 Debian-10+deb9u6",
	}

	if cfg.HostKeyPath != "" {
		f, err := os.Open(cfg.HostKeyPath)
		if err != nil {
			return nil, err
		}
		pemBytes, err := io.ReadAll(f)
		if err != nil {
			return nil, err
		}
		signer, err := gossh.ParsePrivateKey(pemBytes)
		if err != nil {
			return nil, err
		}

		hs.sshServer.AddHostKey(signer)
	}

	return &hs, nil
}

func (hs *HoneypotServer) ListenAndServe() error {
	return hs.sshServer.ListenAndServe()
}

func (hs *HoneypotServer) Shutdown(ctx context.Context) error {
	return hs.sshServer.Shutdown(ctx)
}

func (hs *HoneypotServer) AddLoginCallback(c func(l models.LoginAttempt)) {
	hs.attemptsCallbacks = append(hs.attemptsCallbacks, c)
}

func (hs *HoneypotServer) passwordHandler(ctx sshlib.Context, password string) bool {
	sessUUID, ok := ctx.Value("uuid").(uuid.UUID)
	if !ok {
		hs.Logger.Warn("Unable to get session UUID")
		return false
	}

	la := models.LoginAttempt{
		Date:             time.Now(),
		RemoteIP:         ipFromAddr(ctx.RemoteAddr().String()),
		Username:         ctx.User(),
		Password:         password,
		SSHClientVersion: ctx.ClientVersion(),
		ConnectionUUID:   sessUUID,
	}
	country := hs.LookupCountry(la.RemoteIP)
	if utf8.RuneCountInString(country) > 2 {
		hs.Logger.Warnw("Too many characters in country", "country", country, "runecount", utf8.RuneCountInString(country))
		country = "??"
	}

	la.Country = country
	hs.Logger.Infow("Login attempt",
		"remote_ip", la.RemoteIP.String(),
		"username", la.Username,
		"password", la.Password,
		"country", la.Country)

	if err := hs.attemptStore.AddAttempt(&la); err != nil {
		hs.Logger.Warnw("Error adding attempt to store", "error", err)
	}

	for _, cFunc := range hs.attemptsCallbacks {
		cFunc(la)
	}

	return false
}

func (s *HoneypotServer) connCallback(ctx sshlib.Context, conn net.Conn) net.Conn {
	throttledConn := newThrottledConn(conn)
	throttledConn.SetSpeed(s.throttleSpeed)
	ctx.SetValue("uuid", throttledConn.ID)
	throttledConn.SetSpeed(s.throttleSpeed)
	return throttledConn
}

func handler(session sshlib.Session) {
	_, _ = io.WriteString(session, "[root@hostname ~]#")
	session.Exit(1)
}

func ipFromAddr(addr string) net.IP {
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return nil
	}

	return net.ParseIP(host)
}
