package ssh

import (
	"context"
	"crypto/md5"
	c "docker4ssh/config"
	"docker4ssh/database"
	"docker4ssh/docker"
	"docker4ssh/terminal"
	"encoding/hex"
	"fmt"
	"go.uber.org/zap"
	"golang.org/x/crypto/ssh"
	"net"
	"regexp"
	"strings"
)

var (
	users = make([]*User, 0)

	profiles       c.Profiles
	dynamicProfile c.Profile
)

type User struct {
	*ssh.ServerConn

	ID        string
	IP        string
	Profile   *c.Profile
	Terminal  *terminal.Terminal
	Container *docker.SimpleContainer
}

func GetUser(ip string) *User {
	for _, user := range users {
		if container := user.Container; container != nil && container.Network.IP == ip {
			return user
		}
	}
	return nil
}

type extras struct {
	containerID string
}

func StartServing(config *c.Config, serverConfig *ssh.ServerConfig) (errChan chan error, closer func() error) {
	errChan = make(chan error, 1)

	var err error
	profiles, err = c.LoadProfileDir(config.Profile.Dir, c.DefaultPreProfileFromConfig(config))
	if err != nil {
		errChan <- err
		return
	}
	zap.S().Debugf("Loaded %d profile(s)", len(profiles))

	if config.Profile.Dynamic.Enable {
		dynamicProfile, err = c.DynamicProfileFromConfig(config, c.DefaultPreProfileFromConfig(config))
		if err != nil {
			errChan <- err
			return
		}
		zap.S().Debugf("Loaded dynamic profile")
	}

	cli, err := docker.InitCli()
	if err != nil {
		errChan <- err
		return
	}
	zap.S().Debugf("Initialized docker cli")

	network, err := docker.InitNetwork(context.Background(), cli, config)
	if err != nil {
		errChan <- err
		return
	}
	zap.S().Debugf("Initialized docker networks")

	client := &docker.Client{
		Client:   cli,
		Database: database.GetDatabase(),
		Network:  network,
	}

	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.SSH.Port))
	if err != nil {
		errChan <- err
		return
	}
	zap.S().Debugf("Created ssh listener")

	var closed bool
	go func() {
		db := database.GetDatabase()

		for {
			conn, err := listener.Accept()
			if err != nil {
				if closed {
					return
				}
				zap.S().Errorf("Failed to accept new ssh user: %v", err)
				continue
			}
			serverConn, chans, requests, err := ssh.NewServerConn(conn, serverConfig)
			if err != nil {
				zap.S().Errorf("Failed to establish new ssh connection: %v", err)
				continue
			}

			idBytes := md5.Sum([]byte(strings.Split(serverConn.User(), ":")[0]))
			idString := hex.EncodeToString(idBytes[:])

			zap.S().Infof("New ssh connection from %s with %s (%s)", serverConn.RemoteAddr().String(), serverConn.ClientVersion(), idString)

			var profile *c.Profile
			if name, ok := serverConn.Permissions.CriticalOptions["profile"]; ok {
				if name == "dynamic" {
					if image, ok := serverConn.Permissions.CriticalOptions["image"]; ok {
						tempDynamicProfile := dynamicProfile
						tempDynamicProfile.Image = image
						profile = &tempDynamicProfile
					}
				}
				if profile == nil {
					if profile, ok = profiles.GetByName(name); !ok {
						zap.S().Errorf("Failed to get profile %s", name)
						continue
					}
				}
			} else if containerID, ok := serverConn.Permissions.CriticalOptions["containerID"]; ok {
				if settings, err := db.SettingsByContainerID(containerID); err == nil {
					profile = &c.Profile{
						NetworkMode:        *settings.NetworkMode,
						Configurable:       *settings.Configurable,
						RunLevel:           *settings.RunLevel,
						StartupInformation: *settings.StartupInformation,
						ExitAfter:          *settings.ExitAfter,
						KeepOnExit:         *settings.KeepOnExit,
						ContainerID:        containerID,
					}
				} else {
					for _, container := range allContainers {
						if container.ContainerID == containerID {
							cconfig := c.GetConfig()
							profile = &c.Profile{
								Password:           regexp.MustCompile(cconfig.Profile.Default.Password),
								NetworkMode:        cconfig.Profile.Default.NetworkMode,
								Configurable:       cconfig.Profile.Default.Configurable,
								RunLevel:           cconfig.Profile.Default.RunLevel,
								StartupInformation: cconfig.Profile.Default.StartupInformation,
								ExitAfter:          cconfig.Profile.Default.ExitAfter,
								KeepOnExit:         cconfig.Profile.Default.KeepOnExit,
								Image:              "",
								ContainerID:        containerID,
							}
						}
					}
				}
			}

			zap.S().Debugf("User %s has profile %s", idString, profile.Name())

			user := &User{
				ServerConn: serverConn,
				ID:         idString,
				Terminal:   &terminal.Terminal{},
				Profile:    profile,
			}
			users = append(users, user)

			go ssh.DiscardRequests(requests)
			go handleChannels(chans, client, user)
		}
	}()

	return errChan, func() error {
		closed = true

		// close all containers
		closeAllContainers(context.Background())

		// close the listener
		return listener.Close()
	}
}