// Package metrics gather anonymous information about the usage of NetBird management
package metrics

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/hashicorp/go-version"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/management/server"
	nbversion "github.com/netbirdio/netbird/version"
)

const (
	// PayloadEvent identifies an event type
	PayloadEvent = "self-hosted stats"
	// payloadEndpoint metrics defaultEndpoint to send anonymous data
	payloadEndpoint = "https://metrics.netbird.io"
	// defaultPushInterval default interval to push metrics
	defaultPushInterval = 24 * time.Hour
	// requestTimeout http request timeout
	requestTimeout = 30 * time.Second
)

type getTokenResponse struct {
	PublicAPIToken string `json:"public_api_token"`
}

type pushPayload struct {
	APIKey     string     `json:"api_key"`
	DistinctID string     `json:"distinct_id"`
	Event      string     `json:"event"`
	Properties properties `json:"properties"`
	Timestamp  time.Time  `json:"timestamp"`
}

// properties metrics to push
type properties map[string]interface{}

// DataSource metric data source
type DataSource interface {
	GetAllAccounts() []*server.Account
}

// ConnManager peer connection manager that holds state for current active connections
type ConnManager interface {
	GetAllConnectedPeers() map[string]struct{}
}

// Worker metrics collector and pusher
type Worker struct {
	ctx         context.Context
	id          string
	dataSource  DataSource
	connManager ConnManager
	startupTime time.Time
	lastRun     time.Time
}

// NewWorker returns a metrics worker
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager) *Worker {
	currentTime := time.Now()
	return &Worker{
		ctx:         ctx,
		id:          id,
		dataSource:  dataSource,
		connManager: connManager,
		startupTime: currentTime,
		lastRun:     currentTime,
	}
}

// Run runs the metrics worker
func (w *Worker) Run() {
	pushTicker := time.NewTicker(defaultPushInterval)
	for {
		select {
		case <-w.ctx.Done():
			return
		case <-pushTicker.C:
			err := w.sendMetrics()
			if err != nil {
				log.Error(err)
			}
			w.lastRun = time.Now()
		}
	}
}

func (w *Worker) sendMetrics() error {
	ctx, cancel := context.WithTimeout(w.ctx, requestTimeout)
	defer cancel()

	apiKey, err := getAPIKey(ctx)
	if err != nil {
		return err
	}

	payload := w.generatePayload(apiKey)

	payloadString, err := buildMetricsPayload(payload)
	if err != nil {
		return err
	}

	httpClient := http.Client{}

	exportJobReq, err := createPostRequest(ctx, payloadEndpoint+"/capture/", payloadString)
	if err != nil {
		return fmt.Errorf("unable to create metrics post request %v", err)
	}

	jobResp, err := httpClient.Do(exportJobReq)
	if err != nil {
		return fmt.Errorf("unable to push metrics %v", err)
	}

	defer func() {
		err = jobResp.Body.Close()
		if err != nil {
			log.Errorf("error while closing update metrics response body: %v", err)
		}
	}()

	if jobResp.StatusCode != 200 {
		return fmt.Errorf("unable to push anonymous metrics, got statusCode %d", jobResp.StatusCode)
	}

	log.Infof("sent anonymous metrics, next push will happen in %s. "+
		"You can disable these metrics by running with flag --disable-anonymous-metrics,"+
		" see more information at https://netbird.io/docs/FAQ/metrics-collection", defaultPushInterval)

	return nil
}

func (w *Worker) generatePayload(apiKey string) pushPayload {
	properties := w.generateProperties()

	return pushPayload{
		APIKey:     apiKey,
		DistinctID: w.id,
		Event:      PayloadEvent,
		Properties: properties,
		Timestamp:  time.Now(),
	}
}

func (w *Worker) generateProperties() properties {
	var (
		uptime             float64
		accounts           int
		users              int
		peers              int
		setupKeysUsage     int
		activePeersLastDay int
		osPeers            map[string]int
		userPeers          int
		rules              int
		groups             int
		routes             int
		nameservers        int
		uiClient           int
		version            string
		peerActiveVersions []string
		osUIClients        map[string]int
	)
	start := time.Now()
	metricsProperties := make(properties)
	osPeers = make(map[string]int)
	osUIClients = make(map[string]int)
	uptime = time.Since(w.startupTime).Seconds()
	connections := w.connManager.GetAllConnectedPeers()
	version = nbversion.NetbirdVersion()

	for _, account := range w.dataSource.GetAllAccounts() {
		accounts++
		users = users + len(account.Users)
		rules = rules + len(account.Rules)
		groups = groups + len(account.Groups)
		routes = routes + len(account.Routes)
		nameservers = nameservers + len(account.NameServerGroups)

		for _, key := range account.SetupKeys {
			setupKeysUsage = setupKeysUsage + key.UsedTimes
		}

		for _, peer := range account.Peers {
			peers++
			if peer.SetupKey == "" {
				userPeers++
			}

			osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
			osCount := osPeers[osKey]
			osPeers[osKey] = osCount + 1

			if peer.Meta.UIVersion != "" {
				uiClient++
				uiOSKey := strings.ToLower(fmt.Sprintf("ui_client_os_%s", peer.Meta.GoOS))
				osUICount := osUIClients[uiOSKey]
				osUIClients[uiOSKey] = osUICount + 1
			}

			_, connected := connections[peer.ID]
			if connected || peer.Status.LastSeen.After(w.lastRun) {
				activePeersLastDay++
				osActiveKey := osKey + "_active"
				osActiveCount := osPeers[osActiveKey]
				osPeers[osActiveKey] = osActiveCount + 1
				peerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)
			}
		}
	}

	minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)
	metricsProperties["uptime"] = uptime
	metricsProperties["accounts"] = accounts
	metricsProperties["users"] = users
	metricsProperties["peers"] = peers
	metricsProperties["setup_keys_usage"] = setupKeysUsage
	metricsProperties["active_peers_last_day"] = activePeersLastDay
	metricsProperties["user_peers"] = userPeers
	metricsProperties["rules"] = rules
	metricsProperties["groups"] = groups
	metricsProperties["routes"] = routes
	metricsProperties["nameservers"] = nameservers
	metricsProperties["version"] = version
	metricsProperties["min_active_peer_version"] = minActivePeerVersion
	metricsProperties["max_active_peer_version"] = maxActivePeerVersion
	metricsProperties["ui_clients"] = uiClient
	for os, count := range osPeers {
		metricsProperties[os] = count
	}

	for os, count := range osUIClients {
		metricsProperties[os] = count
	}

	metricsProperties["metric_generation_time"] = time.Since(start).Milliseconds()

	return metricsProperties
}

func getAPIKey(ctx context.Context) (string, error) {

	httpClient := http.Client{}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+"/GetToken", nil)
	if err != nil {
		return "", fmt.Errorf("unable to create request for metrics public api token %v", err)
	}
	response, err := httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("unable to request metrics public api token %v", err)
	}

	defer func() {
		err = response.Body.Close()
		if err != nil {
			log.Errorf("error while closing metrics token response body: %v", err)
		}
	}()

	if response.StatusCode != 200 {
		return "", fmt.Errorf("unable to retrieve metrics token, statusCode %d", response.StatusCode)
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", fmt.Errorf("coudln't get metrics token response; %v", err)
	}

	var tokenResponse getTokenResponse

	err = json.Unmarshal(body, &tokenResponse)
	if err != nil {
		return "", fmt.Errorf("coudln't parse metrics public api token; %v", err)
	}

	return tokenResponse.PublicAPIToken, nil
}

func buildMetricsPayload(payload pushPayload) (string, error) {
	str, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("unable to marshal metrics payload, got err: %v", err)
	}
	return string(str), nil
}

func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, error) {
	reqURL := endpoint

	payload := strings.NewReader(payloadStr)

	req, err := http.NewRequestWithContext(ctx, "POST", reqURL, payload)
	if err != nil {
		return nil, err
	}
	req.Header.Add("content-type", "application/json")

	return req, nil
}

func getMinMaxVersion(inputList []string) (string, string) {
	reg, err := regexp.Compile(version.SemverRegexpRaw)
	if err != nil {
		return "", ""
	}

	versions := make([]*version.Version, 0)

	for _, raw := range inputList {
		if raw != "" && reg.MatchString(raw) {
			v, err := version.NewVersion(raw)
			if err == nil {
				versions = append(versions, v)
			}
		}
	}
	switch len(versions) {
	case 0:
		return "", ""
	case 1:
		v := versions[0].String()
		return v, v
	default:
		sort.Sort(version.Collection(versions))
		return versions[0].String(), versions[len(versions)-1].String()
	}
}
