package ssh

import (
	"context"
	"fmt"
	"os"
	"sync"

	"github.com/edgelesssys/constellation/internal/deploy/user"
	"github.com/edgelesssys/constellation/internal/logger"
	"go.uber.org/zap"
)

// UserKey describes an user that should be created with a corresponding public SSH key.
type UserKey struct {
	Username  string
	PublicKey string
}

// Access reads SSH public keys from a channel, creates the specified users if required and writes the public keys to the users authorized_keys file.
type Access struct {
	log         *logger.Logger
	userManager user.LinuxUserManager
	authorized  map[UserKey]bool
	mux         sync.Mutex
}

// NewAccess creates a new Access.
func NewAccess(log *logger.Logger, userManager user.LinuxUserManager) *Access {
	return &Access{
		log:         log,
		userManager: userManager,
		authorized:  map[UserKey]bool{},
	}
}

// alreadyAuthorized checks if key was written to authorized keys before.
func (s *Access) alreadyAuthorized(sshKey UserKey) bool {
	_, ok := s.authorized[sshKey]
	return ok
}

// rememberAuthorized marks this key as already written to authorized keys..
func (s *Access) rememberAuthorized(sshKey UserKey) {
	s.authorized[sshKey] = true
}

// GetAuthorizedKeys returns a list of authorized keys for the specified user.
func (s *Access) GetAuthorizedKeys() []UserKey {
	s.mux.Lock()
	defer s.mux.Unlock()

	var authorizedKeys []UserKey
	for key := range s.authorized {
		authorizedKeys = append(authorizedKeys, key)
	}

	return authorizedKeys
}

// DeployAuthorizedKey takes an user & public key pair, creates the user if required and deploy a SSH key for them.
func (s *Access) DeployAuthorizedKey(ctx context.Context, sshKey UserKey) error {
	// allow only one thread to write to authorized keys, create users and update the authorized map at a time
	s.mux.Lock()
	defer s.mux.Unlock()
	if s.alreadyAuthorized(sshKey) {
		return nil
	}
	s.log.With(zap.String("username", sshKey.Username)).Infof("Trying to deploy ssh key for user")
	user, err := s.userManager.EnsureLinuxUserExists(ctx, sshKey.Username)
	if err != nil {
		return err
	}
	// CoreOS uses https://github.com/coreos/ssh-key-dir to search for ssh keys in ~/.ssh/authorized_keys.d/*
	sshFolder := fmt.Sprintf("%s/.ssh", user.Home)
	authorizedKeysD := fmt.Sprintf("%s/authorized_keys.d", sshFolder)
	if err := s.userManager.Fs.MkdirAll(authorizedKeysD, 0o700); err != nil {
		return err
	}
	if err := s.userManager.Fs.Chown(sshFolder, user.UID, user.GID); err != nil {
		return err
	}
	if err := s.userManager.Fs.Chown(authorizedKeysD, user.UID, user.GID); err != nil {
		return err
	}
	authorizedKeysPath := fmt.Sprintf("%s/constellation-ssh-keys", authorizedKeysD)
	authorizedKeysFile, err := s.userManager.Fs.OpenFile(authorizedKeysPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	_, err = authorizedKeysFile.WriteString(fmt.Sprintf("%s\n", sshKey.PublicKey))
	if err != nil {
		return err
	}
	if err := authorizedKeysFile.Close(); err != nil {
		return err
	}
	if err := s.userManager.Fs.Chown(authorizedKeysPath, user.UID, user.GID); err != nil {
		return err
	}
	if err := s.userManager.Fs.Chmod(authorizedKeysPath, 0o644); err != nil {
		return err
	}
	s.rememberAuthorized(sshKey)
	s.log.With(zap.String("username", sshKey.Username)).Infof("Successfully authorized user")
	return nil
}
