package server

import (
	"context"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/management/proto"
)

// TURNCredentialsManager used to manage TURN credentials
type TURNCredentialsManager interface {
	GenerateCredentials() TURNCredentials
	SetupRefresh(ctx context.Context, peerKey string)
	CancelRefresh(peerKey string)
}

// TimeBasedAuthSecretsManager generates credentials with TTL and using pre-shared secret known to TURN server
type TimeBasedAuthSecretsManager struct {
	mux           sync.Mutex
	config        *TURNConfig
	updateManager *PeersUpdateManager
	cancelMap     map[string]chan struct{}
}

type TURNCredentials struct {
	Username string
	Password string
}

func NewTimeBasedAuthSecretsManager(updateManager *PeersUpdateManager, config *TURNConfig) *TimeBasedAuthSecretsManager {
	return &TimeBasedAuthSecretsManager{
		mux:           sync.Mutex{},
		config:        config,
		updateManager: updateManager,
		cancelMap:     make(map[string]chan struct{}),
	}
}

// GenerateCredentials generates new time-based secret credentials - basically username is a unix timestamp and password is a HMAC hash of a timestamp with a preshared TURN secret
func (m *TimeBasedAuthSecretsManager) GenerateCredentials() TURNCredentials {
	mac := hmac.New(sha1.New, []byte(m.config.Secret))

	timeAuth := time.Now().Add(m.config.CredentialsTTL.Duration).Unix()

	username := fmt.Sprint(timeAuth)

	_, err := mac.Write([]byte(username))
	if err != nil {
		log.Errorln("Generating turn password failed with error: ", err)
	}

	bytePassword := mac.Sum(nil)
	password := base64.StdEncoding.EncodeToString(bytePassword)

	return TURNCredentials{
		Username: username,
		Password: password,
	}

}

func (m *TimeBasedAuthSecretsManager) cancel(peerID string) {
	if channel, ok := m.cancelMap[peerID]; ok {
		close(channel)
		delete(m.cancelMap, peerID)
	}
}

// CancelRefresh cancels scheduled peer credentials refresh
func (m *TimeBasedAuthSecretsManager) CancelRefresh(peerID string) {
	m.mux.Lock()
	defer m.mux.Unlock()
	m.cancel(peerID)
}

// SetupRefresh starts peer credentials refresh. Since credentials are expiring (TTL) it is necessary to always generate them and send to the peer.
// A goroutine is created and put into TimeBasedAuthSecretsManager.cancelMap. This routine should be cancelled if peer is gone.
func (m *TimeBasedAuthSecretsManager) SetupRefresh(ctx context.Context, peerID string) {
	m.mux.Lock()
	defer m.mux.Unlock()
	m.cancel(peerID)
	cancel := make(chan struct{}, 1)
	m.cancelMap[peerID] = cancel
	log.WithContext(ctx).Debugf("starting turn refresh for %s", peerID)

	go func() {
		// we don't want to regenerate credentials right on expiration, so we do it slightly before (at 3/4 of TTL)
		ticker := time.NewTicker(m.config.CredentialsTTL.Duration / 4 * 3)

		for {
			select {
			case <-cancel:
				log.WithContext(ctx).Debugf("stopping turn refresh for %s", peerID)
				return
			case <-ticker.C:
				c := m.GenerateCredentials()
				var turns []*proto.ProtectedHostConfig
				for _, host := range m.config.Turns {
					turns = append(turns, &proto.ProtectedHostConfig{
						HostConfig: &proto.HostConfig{
							Uri:      host.URI,
							Protocol: ToResponseProto(host.Proto),
						},
						User:     c.Username,
						Password: c.Password,
					})
				}

				update := &proto.SyncResponse{
					WiretrusteeConfig: &proto.WiretrusteeConfig{
						Turns: turns,
					},
				}
				log.WithContext(ctx).Debugf("sending new TURN credentials to peer %s", peerID)
				m.updateManager.SendUpdate(ctx, peerID, &UpdateMessage{Update: update})
			}
		}
	}()
}
