// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023 Datadog, Inc.

package internal

import (
	"net/url"
	"runtime/debug"
	"sync"

	"github.com/DataDog/dd-trace-go/v2/internal/env"
	"github.com/DataDog/dd-trace-go/v2/internal/log"
)

const (
	// EnvGitMetadataEnabledFlag specifies the environment variable name for enable/disable
	EnvGitMetadataEnabledFlag = "DD_TRACE_GIT_METADATA_ENABLED"
	// EnvGitRepositoryURL specifies the environment variable name for git repository URL
	EnvGitRepositoryURL = "DD_GIT_REPOSITORY_URL"
	// EnvGitCommitSha specifies the environment variable name git commit sha
	EnvGitCommitSha = "DD_GIT_COMMIT_SHA"
	// EnvDDTags specifies the environment variable name global tags
	EnvDDTags = "DD_TAGS"

	// TagRepositoryURL specifies the tag name for git repository URL
	TagRepositoryURL = "git.repository_url"
	// TagCommitSha specifies the tag name for git commit sha
	TagCommitSha = "git.commit.sha"
	// TagGoPath specifies the tag name for go module path
	TagGoPath = "go_path"

	// TraceTagRepositoryURL specifies the trace tag name for git repository URL
	TraceTagRepositoryURL = "_dd.git.repository_url"
	// TraceTagCommitSha specifies the trace tag name for git commit sha
	TraceTagCommitSha = "_dd.git.commit.sha"
	// TraceTagGoPath specifies the trace tag name for go module path
	TraceTagGoPath = "_dd.go_path"
)

var (
	initOnce        sync.Once
	gitMetadataTags map[string]string
)

func updateTags(tags map[string]string, key string, value string) {
	if _, ok := tags[key]; !ok && value != "" {
		tags[key] = value
	}
}

func updateAllTags(tags map[string]string, newtags map[string]string) {
	for k, v := range newtags {
		updateTags(tags, k, v)
	}
}

// Get git metadata from environment variables
func getTagsFromEnv() map[string]string {
	return map[string]string{
		TagRepositoryURL: removeCredentials(env.Get(EnvGitRepositoryURL)),
		TagCommitSha:     env.Get(EnvGitCommitSha),
	}
}

// Get git metadata from DD_TAGS
func getTagsFromDDTags() map[string]string {
	etags := ParseTagString(env.Get(EnvDDTags))

	return map[string]string{
		TagRepositoryURL: removeCredentials(etags[TagRepositoryURL]),
		TagCommitSha:     etags[TagCommitSha],
		TagGoPath:        etags[TagGoPath],
	}
}

// getTagsFromBinary extracts git metadata from binary metadata.
func getTagsFromBinary(readBuildInfo func() (*debug.BuildInfo, bool)) map[string]string {
	res := make(map[string]string)
	info, ok := readBuildInfo()
	if !ok {
		log.Debug("ReadBuildInfo failed, skip source code metadata extracting")
		return res
	}
	goPath := info.Path
	var vcs, commitSha string
	for _, s := range info.Settings {
		if s.Key == "vcs" {
			vcs = s.Value
		} else if s.Key == "vcs.revision" {
			commitSha = s.Value
		}
	}
	if vcs != "git" {
		log.Debug("Unknown VCS: '%s', skip source code metadata extracting", vcs)
		return res
	}
	res[TagCommitSha] = commitSha
	res[TagGoPath] = goPath
	return res
}

// GetGitMetadataTags returns git metadata tags. Returned map is read-only
func GetGitMetadataTags() map[string]string {
	initOnce.Do(initGitMetadataTags)
	return gitMetadataTags
}

func initGitMetadataTags() {
	gitMetadataTags = make(map[string]string)

	if BoolEnv(EnvGitMetadataEnabledFlag, true) {
		updateAllTags(gitMetadataTags, getTagsFromEnv())
		updateAllTags(gitMetadataTags, getTagsFromDDTags())
		updateAllTags(gitMetadataTags, getTagsFromBinary(debug.ReadBuildInfo))
	}
}

// RefreshGitMetadataTags reset cached metadata tags. NOT thread-safe, use for testing only
func RefreshGitMetadataTags() {
	initGitMetadataTags()
}

// CleanGitMetadataTags cleans up tags from git metadata
func CleanGitMetadataTags(tags map[string]string) {
	delete(tags, TagRepositoryURL)
	delete(tags, TagCommitSha)
	delete(tags, TagGoPath)
}

// GetTracerGitMetadataTags returns git metadata tags for tracer
// NB: Currently tracer inject tags with some workaround
// (only with _dd prefix and only for the first span in payload)
// So we provide different tag names
func GetTracerGitMetadataTags() map[string]string {
	res := make(map[string]string)
	tags := GetGitMetadataTags()

	updateTags(res, TraceTagRepositoryURL, tags[TagRepositoryURL])
	updateTags(res, TraceTagCommitSha, tags[TagCommitSha])
	updateTags(res, TraceTagGoPath, tags[TagGoPath])

	return res
}

// removeCredentials returns the passed url with potential credentials removed.
// If the input string is not a valid URL, the string is returned as is.
func removeCredentials(urlStr string) string {
	if urlStr == "" {
		return urlStr
	}
	u, err := url.Parse(urlStr)
	if err != nil {
		// not an url, nothing to remove
		return urlStr
	}
	if u.User == nil {
		// nothing to remove
		return urlStr
	}
	u.User = nil
	return u.String()
}
