// 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 tools

import (
	"fmt"
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"os"
	"reflect"
	"sort"
	"strings"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"

	"github.com/apache/skywalking-satellite/plugins/queue/partition"

	"github.com/apache/skywalking-satellite/internal/pkg/log"
	"github.com/apache/skywalking-satellite/internal/pkg/plugin"
	"github.com/apache/skywalking-satellite/plugins"
	fetcher_api "github.com/apache/skywalking-satellite/plugins/fetcher/api"
	forwarder_api "github.com/apache/skywalking-satellite/plugins/forwarder/api"
	receiver_api "github.com/apache/skywalking-satellite/plugins/receiver/api"

	"golang.org/x/mod/modfile"
)

const (
	topLevel       = "# "
	secondLevel    = "## "
	lf             = "\n"
	yamlQuoteStart = "```yaml"
	yamlQuoteEnd   = "```"
	markdownSuffix = ".md"

	commentPrefix = "/ "

	categoryQueue = "Queue"
)

func GeneratePluginDoc(outputRootPath, menuFilePath, pluginFilePath string) error {
	log.Init(&log.LoggerConfig{})
	plugins.RegisterPlugins()

	pluginPath := fmt.Sprintf("%s%s", outputRootPath, pluginFilePath)
	if err := createDir(pluginPath); err != nil {
		return fmt.Errorf("create docs dir error: %v", err)
	}
	if err := generatePluginListDoc(pluginPath, getSortedCategories()); err != nil {
		return err
	}
	if err := updateMenuPluginListDoc(outputRootPath, menuFilePath, pluginFilePath, getSortedCategories()); err != nil {
		return err
	}
	log.Logger.Info("Successfully generate documentation!")
	return nil
}

// sort categories by dictionary sequence
func getSortedCategories() []reflect.Type {
	var categories []reflect.Type
	for c := range plugin.Reg {
		categories = append(categories, c)
	}
	sort.Slice(categories, func(i, j int) bool {
		return strings.Compare(categories[i].String(), categories[j].String()) <= 0
	})
	return categories
}

func updateMenuPluginListDoc(outputRootPath, menuFilePath, pluginFilePath string, categories []reflect.Type) error {
	menuFile := fmt.Sprintf("%s%s", outputRootPath, menuFilePath)
	menu, err := LoadCatalog(menuFile)
	if err != nil {
		return err
	}

	// find plugin Catalog
	pluginCatalog := menu.Find("Setup", "Plugins")
	if pluginCatalog == nil {
		return fmt.Errorf("cannot find plugins Catalog")
	}

	// remove path
	pluginCatalog.Path = ""

	// rebuild all plugins
	var allPlugins []*Catalog
	for _, category := range categories {
		// plugin
		implements := []*Catalog{}
		curPlugin := &Catalog{
			Name: cases.Title(language.Und, cases.NoLower).String(strings.ToLower(category.Name())),
		}

		// all implements
		pluginList := getPluginsByCategory(category)
		for _, pluginName := range pluginList {
			implements = append(implements, &Catalog{
				Name: pluginName.showName,
				Path: strings.TrimRight(fmt.Sprintf("%s/%s", pluginFilePath, getPluginDocFileName(category, pluginName.defineName)), markdownSuffix),
			})
		}
		curPlugin.Catalog = implements

		if len(implements) > 0 {
			allPlugins = append(allPlugins, curPlugin)
		}
	}
	pluginCatalog.Catalog = allPlugins

	return menu.Save(menuFile)
}

func generatePluginListDoc(docDir string, categories []reflect.Type) error {
	fileName := docDir + "/" + "plugin-list" + markdownSuffix
	docStr := topLevel + "Plugin List" + lf
	for _, category := range categories {
		docStr += "- " + category.Name() + lf
		pluginList := getPluginsByCategory(category)
		for _, pluginName := range pluginList {
			docStr += "	- [" + pluginName.showName + "](./" + getPluginDocFileName(category, pluginName.defineName) + ")" + lf
			if err := generatePluginDoc(docDir, category, pluginName); err != nil {
				return err
			}
		}
	}
	return writeDoc([]byte(docStr), fileName)
}

func generatePluginDoc(docDir string, category reflect.Type, pluginName *pluginNames) error {
	docFileName := docDir + "/" + getPluginDocFileName(category, pluginName.defineName)
	p := plugin.Get(category, plugin.Config{plugin.NameField: pluginName.defineName})
	docRes := topLevel + category.Name() + "/" + pluginName.defineName + lf
	docRes += secondLevel + "Description" + lf
	docRes += p.Description() + lf
	docRes += generateSupportForwarders(category, p)
	docRes += secondLevel + "DefaultConfig" + lf
	docRes += yamlQuoteStart + generateDefaultConfig(category, p) + yamlQuoteEnd + lf
	docRes += secondLevel + "Configuration" + lf
	docRes += generateConfiguration(category, p) + lf
	return writeDoc([]byte(docRes), docFileName)
}

func GetModuleName() string {
	goModBytes, err := os.ReadFile("go.mod")
	if err != nil {
		return ""
	}

	modName := modfile.ModulePath(goModBytes)
	return modName
}

func generateDefaultConfig(category reflect.Type, p plugin.Plugin) string {
	configs := p.DefaultConfig()
	if category.Name() == categoryQueue {
		partitionQueue := &partition.PartitionedQueue{}
		configs = fmt.Sprintf("%s%s", configs, partitionQueue.DefaultConfig())
	}
	return configs
}

func generateConfiguration(category reflect.Type, p plugin.Plugin) string {
	var content = ""

	content += "|Name|Type|Description|" + lf
	content += "|----|----|-----------|" + lf

	configurations := getConfigurations(category, reflect.TypeOf(p).Elem())
	eachConfigurationItem(configurations, "", func(name, dataType, desc string) {
		content += fmt.Sprintf("| %s | %s | %s |%s", name, dataType, desc, lf)
	})
	if category.Name() == categoryQueue {
		configurations := getConfigurations(category, reflect.TypeOf(&partition.PartitionedQueue{}).Elem())
		eachConfigurationItem(configurations, "", func(name, dataType, desc string) {
			content += fmt.Sprintf("| %s | %s | %s |%s", name, dataType, desc, lf)
		})
	}

	return content
}

func eachConfigurationItem(items []*pluginConfigurationItem, parentName string, consumer func(name, dataType, desc string)) {
	for _, conf := range items {
		consumer(parentName+conf.name, conf.dataType, conf.description)
		eachConfigurationItem(conf.children, parentName+conf.name+".", consumer)
	}
}

type pluginConfigurationItem struct {
	name        string
	description string
	dataType    string
	children    []*pluginConfigurationItem
}

type pluginChildrenFinder struct {
	childType reflect.Type
	squash    bool
}

func getConfigurations(category, p reflect.Type) []*pluginConfigurationItem {
	pluginDir := strings.TrimPrefix(p.PkgPath(), GetModuleName())
	fset := token.NewFileSet()

	d, err := parser.ParseDir(fset, "."+pluginDir, nil, parser.ParseComments)
	if err != nil {
		log.Logger.Warnf("failed to generate plugin [%s] configuration, error: %v", category.Name()+"/"+p.Name(), err)
		return make([]*pluginConfigurationItem, 0)
	}

	result := make([]*pluginConfigurationItem, 0)
	for _, f := range d {
		pack := doc.New(f, "./", 0)
		for _, t := range pack.Types {
			if t.Name != p.Name() {
				continue
			}

			for _, spec := range t.Decl.Specs {
				typeSpec, ok := spec.(*ast.TypeSpec)
				if !ok {
					continue
				}
				structType, ok := typeSpec.Type.(*ast.StructType)
				if !ok {
					continue
				}

				for _, field := range structType.Fields.List {
					item, childFinder := parsePluginConfigurationItem(field, p)
					if childFinder != nil {
						configurations := getConfigurations(category, childFinder.childType)
						if childFinder.squash {
							result = append(result, configurations...)
						} else if item != nil {
							item.children = configurations
						}
					}
					if item != nil {
						result = append(result, item)
					}
				}
			}
		}
	}
	return result
}

// parse field to configuration item
func parsePluginConfigurationItem(field *ast.Field, pType reflect.Type) (*pluginConfigurationItem, *pluginChildrenFinder) {
	if field.Tag == nil {
		return nil, nil
	}

	var fieldName = ""
	if field.Names != nil {
		for _, n := range field.Names {
			fieldName += n.Name
		}
	} else {
		expr, ok := field.Type.(*ast.SelectorExpr)
		if !ok {
			return nil, nil
		}
		fieldName = expr.Sel.Name
	}

	pluginField, find := pType.FieldByName(fieldName)
	if !find {
		return nil, nil
	}
	mapStructureValue := pluginField.Tag.Get("mapstructure")
	var confName string
	var childrenFinder *pluginChildrenFinder
	if index := strings.Index(mapStructureValue, ","); index != -1 {
		if strings.Contains(mapStructureValue[index+1:], "squash") {
			if pluginField.Type.Kind() == reflect.Struct {
				return nil, &pluginChildrenFinder{childType: pluginField.Type, squash: true}
			}
			log.Logger.Warnf("Could not identity plugin field: %v", pluginField)
			return nil, nil
		}
		confName = mapStructureValue[:index]
	} else if mapStructureValue != "" {
		confName = mapStructureValue
	} else {
		confName = fieldName
	}

	var dataType = pluginField.Type.String()
	switch pluginField.Type.Kind() {
	case reflect.Struct:
	case reflect.Ptr:
		if pluginField.Type.Elem().PkgPath() != "" {
			childrenFinder = &pluginChildrenFinder{childType: pluginField.Type.Elem()}
		}
	}

	return &pluginConfigurationItem{
		name:        confName,
		dataType:    dataType,
		description: buildPluginDescription(field),
	}, childrenFinder
}

func buildPluginDescription(field *ast.Field) string {
	var comments = ""
	for _, group := range []*ast.CommentGroup{field.Doc, field.Comment} {
		if group != nil {
			for _, comment := range group.List {
				comments += strings.TrimLeft(comment.Text, commentPrefix)
			}
		}
	}
	return comments
}

func generateSupportForwarders(category reflect.Type, p plugin.Plugin) string {
	var forwarders []forwarder_api.Forwarder
	if category.Name() == "Receiver" {
		forwarders = p.(receiver_api.Receiver).SupportForwarders()
	} else if category.Name() == "Fetcher" {
		forwarders = p.(fetcher_api.Fetcher).SupportForwarders()
	}
	if len(forwarders) == 0 {
		return ""
	}
	result := secondLevel + "Support Forwarders" + lf
	for _, forwarder := range forwarders {
		result += " - [" + forwarder.Name() + "](" + getPluginDocFileName(reflect.TypeOf(forwarder).Elem(), forwarder.Name()) + ")" + lf
	}
	return result
}

type pluginNames struct {
	defineName string
	showName   string
}

func getPluginsByCategory(category reflect.Type) []*pluginNames {
	mapping := plugin.Reg[category]
	var keys []*pluginNames
	for k := range mapping {
		t := mapping[k].Type()
		if t.Kind() == reflect.Ptr {
			t = t.Elem()
		}
		p := reflect.New(t).Interface().(plugin.Plugin)
		keys = append(keys, &pluginNames{defineName: k, showName: p.ShowName()})
	}
	sort.Slice(keys, func(i, j int) bool {
		return keys[i].defineName < keys[j].defineName
	})
	return keys
}

func getPluginDocFileName(category reflect.Type, pluginName string) string {
	return strings.ToLower(category.Name() + "_" + pluginName + markdownSuffix)
}

func writeDoc(docBytes []byte, docFileName string) error {
	if err := os.WriteFile(docFileName, docBytes, 0o600); err != nil {
		return fmt.Errorf("cannot init the plugin doc: %v", err)
	}
	return nil
}

func createDir(path string) error {
	err := os.RemoveAll(path)
	if err != nil {
		return err
	}
	fileInfo, err := os.Stat(path)
	if os.IsNotExist(err) || fileInfo.Size() == 0 {
		return os.Mkdir(path, os.ModePerm)
	}
	return err
}
