// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package dashboard

import (
	"bytes"
	"context"
	"os"
	"strings"
	"sync"

	"github.com/apache/skywalking-cli/pkg/contextkey"
	"github.com/apache/skywalking-cli/pkg/display/displayable"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"
	api "skywalking.apache.org/repo/goapi/query"

	"github.com/spf13/viper"

	"gopkg.in/yaml.v2"

	"github.com/apache/skywalking-cli/assets"
	"github.com/apache/skywalking-cli/pkg/graphql/metrics"
	"github.com/apache/skywalking-cli/pkg/graphql/utils"
	"github.com/apache/skywalking-cli/pkg/logger"
)

type ButtonTemplate struct {
	Texts    []string `mapstructure:"texts"`
	ColorNum int      `mapstructure:"colorNumber"`
	Height   int      `mapstructure:"height"`
}

type MetricTemplate struct {
	Condition      api.TopNCondition `mapstructure:"condition"`
	Title          string            `mapstructure:"title"`
	Aggregation    string            `mapstructure:"aggregation"`
	AggregationNum string            `mapstructure:"aggregationNum"`
}

type ChartTemplate struct {
	Condition api.MetricsCondition `mapstructure:"condition"`
	Title     string               `mapstructure:"title"`
	Unit      string               `mapstructure:"unit"`
	Relabels  string               `mapstructure:"relabels"`
	Labels    string               `mapstructure:"labels"`
}

type GlobalTemplate struct {
	Buttons         ButtonTemplate   `mapstructure:"buttons"`
	Metrics         []MetricTemplate `mapstructure:"metrics"`
	ResponseLatency ChartTemplate    `mapstructure:"responseLatency"`
	HeatMap         ChartTemplate    `mapstructure:"heatMap"`
}

type GlobalData struct {
	Metrics         [][]*api.SelectedRecord                        `json:"metrics"`
	ResponseLatency map[string]map[string]*displayable.MetricValue `json:"responseLatency"`
	HeatMap         api.HeatMap                                    `json:"heatMap"`
}

// Use singleton pattern to make sure to load template only once.
var globalTemplate *GlobalTemplate

const (
	templateType        = "yml"
	DefaultTemplatePath = "templates/dashboard/global.yml"
)

// newGlobalTemplate create a new GlobalTemplate and set default values for buttons' template.
func newGlobalTemplate() GlobalTemplate {
	return GlobalTemplate{
		Buttons: ButtonTemplate{
			ColorNum: 220,
			Height:   1,
		},
	}
}

// LoadTemplate reads UI template from yaml file.
func LoadTemplate(filename string) (*GlobalTemplate, error) {
	if globalTemplate != nil {
		return globalTemplate, nil
	}

	gt := newGlobalTemplate()
	viper.SetConfigType(templateType)

	var err error
	var byteValue []byte
	if filename == DefaultTemplatePath {
		byteValue = []byte(assets.Read(filename))
	} else {
		byteValue, err = os.ReadFile(filename)
		if err != nil {
			return nil, err
		}
	}

	gt.Buttons.Texts, err = getButtonTexts(byteValue)
	if err != nil {
		return nil, err
	}

	if err := viper.ReadConfig(bytes.NewReader(byteValue)); err != nil {
		return nil, err
	}
	if err := viper.Unmarshal(&gt); err != nil {
		return nil, err
	}

	globalTemplate = &gt
	return globalTemplate, nil
}

// getButtonTexts get keys in the template file,
// which will be set as texts of buttons in the dashboard.
func getButtonTexts(byteValue []byte) ([]string, error) {
	var ret []string

	c := make(map[string]any)
	if err := yaml.Unmarshal(byteValue, &c); err != nil {
		return nil, err
	}

	for s := range c {
		if s != "style" {
			ret = append(ret, cases.Title(language.Und).String(s))
		}
	}
	return ret, nil
}

func Metrics(ctx context.Context, duration api.Duration) ([][]*api.SelectedRecord, error) {
	var ret [][]*api.SelectedRecord

	template, err := LoadTemplate(ctx.Value(contextkey.DashboardTemplate{}).(string))
	if err != nil {
		return nil, nil
	}

	// Check if there is a template of metrics.
	if template.Metrics == nil {
		return nil, nil
	}

	for _, m := range template.Metrics {
		sortMetrics, err := metrics.SortMetrics(ctx, m.Condition, duration)
		if err != nil {
			return nil, err
		}
		ret = append(ret, sortMetrics)
	}

	return ret, nil
}

func responseLatency(ctx context.Context, duration api.Duration) map[string]map[string]*displayable.MetricValue {
	template, err := LoadTemplate(ctx.Value(contextkey.DashboardTemplate{}).(string))
	if err != nil {
		return nil
	}

	// Check if there is a template of response latency.
	if template.ResponseLatency == (ChartTemplate{}) {
		return nil
	}

	// Labels in the template file is string type, like "0, 1, 2",
	// need use ", " to split into string array for graphql query.
	labels := strings.Split(template.ResponseLatency.Labels, ",")
	relabels := strings.Split(template.ResponseLatency.Relabels, ",")

	responseLatency, err := metrics.MultipleLinearIntValues(ctx, template.ResponseLatency.Condition, labels, duration)
	if err != nil {
		logger.Log.Fatalln(err)
	}

	mapping := make(map[string]string, len(labels))
	for i := range labels {
		mapping[labels[i]] = relabels[i]
	}

	// Convert metrics values to map type data.
	return utils.MetricsValuesArrayToMap(duration, responseLatency, mapping)
}

func heatMap(ctx context.Context, duration api.Duration) (api.HeatMap, error) {
	template, err := LoadTemplate(ctx.Value(contextkey.DashboardTemplate{}).(string))
	if err != nil {
		return api.HeatMap{}, nil
	}

	// Check if there is a template of heat map.
	if template.HeatMap == (ChartTemplate{}) {
		return api.HeatMap{}, nil
	}

	return metrics.Thermodynamic(ctx, template.HeatMap.Condition, duration)
}

func Global(ctx context.Context, duration api.Duration) (*GlobalData, error) {
	// Load template to `globalTemplate`, so the subsequent three calls can ues it directly.
	_, err := LoadTemplate(ctx.Value(contextkey.DashboardTemplate{}).(string))
	if err != nil {
		return nil, nil
	}

	errors := make(chan error)
	done := make(chan bool)

	// Use three goroutines to enable concurrent execution of three graphql queries.
	var wg sync.WaitGroup
	wg.Add(3)
	var m [][]*api.SelectedRecord
	go func() {
		m, err = Metrics(ctx, duration)
		if err != nil {
			errors <- err
		}
		wg.Done()
	}()
	var rl map[string]map[string]*displayable.MetricValue
	go func() {
		rl = responseLatency(ctx, duration)
		wg.Done()
	}()
	var hm api.HeatMap
	go func() {
		hm, err = heatMap(ctx, duration)
		if err != nil {
			errors <- err
		}
		wg.Done()
	}()

	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		break
	case err := <-errors:
		close(errors)
		return nil, err
	}

	var globalData GlobalData
	globalData.Metrics = m
	globalData.ResponseLatency = rl
	globalData.HeatMap = hm

	return &globalData, nil
}
