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

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"

	"github.com/apache/skywalking-rover/pkg/logger"
	host2 "github.com/apache/skywalking-rover/pkg/tools/host"
	"github.com/apache/skywalking-rover/pkg/tools/path"
	"github.com/apache/skywalking-rover/pkg/tools/profiling"
)

var (
	// NotSupportProfilingExe mean which program are not support for profiling
	// Not Support JIT and Script language for now
	NotSupportProfilingExe = []string{
		"java", "python", "node", "bash", "ruby", "ssh",
	}

	// executable file profiling finders
	profilingStatFinderList = []profiling.StatFinder{
		profiling.NewGoLibrary(),
	}

	// kernel profiling finder
	kernelFinder = profiling.NewKernelFinder()

	// process map file analyze(/proc/{pid}/maps)
	mapFileContentRegex = regexp.MustCompile("(?P<StartAddr>[a-f\\d]+)\\-(?P<EndAddr>[a-f\\d]+)\\s(?P<Perm>[^\\s]+)" +
		"\\s(?P<Offset>[a-f\\d]+)\\s[a-f\\d]+\\:[a-f\\d]+\\s\\d+\\s+(?P<Name>[^\\n]+)")

	log = logger.GetLogger("tools", "process")
)

// KernelFileProfilingStat is works for read the kernel and get is support for kernel symbol analyze
func KernelFileProfilingStat() (*profiling.Info, error) {
	if !kernelFinder.IsSupport(profiling.KernelProcSymbolFilePath) {
		return nil, fmt.Errorf("not support kernel space profiling")
	}
	return kernelFinder.Analyze(profiling.KernelProcSymbolFilePath)
}

// ProfilingStat is validating the exe file could be profiling and get info
func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) {
	stat, err := os.Stat(exePath)
	if err != nil {
		return nil, fmt.Errorf("check file error: %v", err)
	}
	for _, notSupport := range NotSupportProfilingExe {
		if strings.HasPrefix(stat.Name(), notSupport) {
			return nil, fmt.Errorf("not support %s language profiling", notSupport)
		}
	}
	context := newAnalyzeContext()

	// the executable file must have the symbols
	symbols, err := context.GetFinder(exePath).AnalyzeSymbols(exePath)
	if err != nil || len(symbols) == 0 {
		return nil, fmt.Errorf("could not found any symbol in the execute file: %s, error: %v", exePath, err)
	}

	return analyzeProfilingInfo(context, pid)
}

// Modules Read the profiling info of the process, without the symbol check
func Modules(pid int32) ([]*profiling.Module, error) {
	context := newAnalyzeContext()
	info, err := analyzeProfilingInfo(context, pid)
	if err != nil {
		return nil, err
	}
	return info.Modules, nil
}

func analyzeProfilingInfo(context *analyzeContext, pid int32) (*profiling.Info, error) {
	// analyze process mapping
	mapFile, _ := os.Open(host2.GetHostProcInHost(fmt.Sprintf("%d/maps", pid)))
	scanner := bufio.NewScanner(mapFile)
	modules := make(map[string]*profiling.Module)
	for scanner.Scan() {
		submatch := mapFileContentRegex.FindStringSubmatch(scanner.Text())
		if len(submatch) != 6 {
			continue
		}
		if len(submatch[3]) > 2 && submatch[3][2] != 'x' {
			continue
		}
		moduleName := submatch[5]
		if isIgnoreModuleName(moduleName) {
			continue
		}

		// parsing range
		var err error
		moduleRange := &profiling.ModuleRange{}
		moduleRange.StartAddr, err = parseUInt64InModule(err, moduleName, "start address", submatch[1])
		moduleRange.EndAddr, err = parseUInt64InModule(err, moduleName, "end address", submatch[2])
		moduleRange.FileOffset, err = parseUInt64InModule(err, moduleName, "file offset", submatch[4])
		if err != nil {
			return nil, err
		}

		module := modules[moduleName]
		if module != nil {
			module.Ranges = append(module.Ranges, moduleRange)
			continue
		}
		modulePath := host2.GetHostProcInHost(fmt.Sprintf("%d/root%s", pid, moduleName))
		if !path.Exists(modulePath) {
			log.Debugf("could not found the module, ignore. name: %s, path: %s", moduleName, modulePath)
			continue
		}

		module, err = context.GetFinder(modulePath).ToModule(pid, moduleName, modulePath, []*profiling.ModuleRange{moduleRange})
		if err != nil {
			if err == profiling.ErrNotSupport {
				log.Warnf("not support the module in process(%d): %s, path: %s", pid, moduleName, modulePath)
				continue
			}
			return nil, fmt.Errorf("could not init the module: %s, error: %v", moduleName, err)
		}
		modules[moduleName] = module
	}
	return profiling.NewInfo(modules), nil
}

func parseUInt64InModule(err error, moduleName, key, val string) (uint64, error) {
	if err != nil {
		return 0, err
	}
	res, err := strconv.ParseUint(val, 16, 64)
	if err != nil {
		return 0, fmt.Errorf("error parsing the %s in maps file: %s", key, moduleName)
	}
	return res, nil
}

func isIgnoreModuleName(name string) bool {
	return name != "" &&
		(strings.HasPrefix(name, "//anon") ||
			strings.HasPrefix(name, "/dev/zero") ||
			strings.HasPrefix(name, "/anon_hugepage") ||
			strings.HasPrefix(name, "[stack") ||
			strings.HasPrefix(name, "/SYSV") ||
			strings.HasPrefix(name, "[heap]") ||
			strings.HasPrefix(name, "/memfd:") ||
			strings.HasPrefix(name, "[vdso]") ||
			strings.HasPrefix(name, "[vsyscall]") ||
			strings.HasPrefix(name, "[uprobes]") ||
			strings.HasSuffix(name, ".map"))
}

type analyzeContext struct {
	pathToFinder map[string]profiling.StatFinder
}

func newAnalyzeContext() *analyzeContext {
	return &analyzeContext{
		pathToFinder: make(map[string]profiling.StatFinder),
	}
}

func (a *analyzeContext) GetFinder(name string) profiling.StatFinder {
	if f := a.pathToFinder[name]; f != nil {
		return f
	}

	// find all finders
	for _, f := range profilingStatFinderList {
		if f.IsSupport(name) {
			a.pathToFinder[name] = f
			return f
		}
	}

	// not support
	n := profiling.NewNotSupport()
	a.pathToFinder[name] = n
	return n
}
