package buildpacks

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"sync"

	"github.com/Masterminds/semver/v3"
	"github.com/google/go-github/v41/github"
	"github.com/xanzy/go-gitlab"
)

var lts = map[string]int{
	"argon":   4,
	"boron":   6,
	"carbon":  8,
	"dubnium": 10,
}

type nodejsRuntime struct {
	wg sync.WaitGroup
}

func NewNodeRuntime() Runtime {
	return &nodejsRuntime{}
}

func (runtime *nodejsRuntime) detectYarnGithub(results chan struct {
	string
	bool
}, directoryContent []*github.RepositoryContent,
) {
	yarnLockFound := false
	packageJSONFound := false
	for i := 0; i < len(directoryContent); i++ {
		name := directoryContent[i].GetName()
		if name == "yarn.lock" {
			yarnLockFound = true
		} else if name == "package.json" {
			packageJSONFound = true
		}
		if yarnLockFound && packageJSONFound {
			break
		}
	}
	if yarnLockFound && packageJSONFound {
		results <- struct {
			string
			bool
		}{yarn, true}
	}
	runtime.wg.Done()
}

func (runtime *nodejsRuntime) detectYarnGitlab(results chan struct {
	string
	bool
}, tree []*gitlab.TreeNode,
) {
	yarnLockFound := false
	packageJSONFound := false
	for i := 0; i < len(tree); i++ {
		name := tree[i].Name
		if name == "yarn.lock" {
			yarnLockFound = true
		} else if name == "package.json" {
			packageJSONFound = true
		}
		if yarnLockFound && packageJSONFound {
			break
		}
	}
	if yarnLockFound && packageJSONFound {
		results <- struct {
			string
			bool
		}{yarn, true}
	}
	runtime.wg.Done()
}

func (runtime *nodejsRuntime) detectNPMGithub(results chan struct {
	string
	bool
}, directoryContent []*github.RepositoryContent,
) {
	packageJSONFound := false
	for i := 0; i < len(directoryContent); i++ {
		name := directoryContent[i].GetName()
		if name == "package.json" {
			packageJSONFound = true
			break
		}
	}
	if packageJSONFound {
		results <- struct {
			string
			bool
		}{npm, true}
	}
	runtime.wg.Done()
}

func (runtime *nodejsRuntime) detectNPMGitlab(results chan struct {
	string
	bool
}, tree []*gitlab.TreeNode,
) {
	packageJSONFound := false
	for i := 0; i < len(tree); i++ {
		name := tree[i].Name
		if name == "package.json" {
			packageJSONFound = true
			break
		}
	}
	if packageJSONFound {
		results <- struct {
			string
			bool
		}{npm, true}
	}
	runtime.wg.Done()
}

func (runtime *nodejsRuntime) detectStandaloneGithub(results chan struct {
	string
	bool
}, directoryContent []*github.RepositoryContent,
) {
	jsFileFound := false
	for i := 0; i < len(directoryContent); i++ {
		name := directoryContent[i].GetName()
		if name == "server.js" || name == "app.js" || name == "main.js" || name == "index.js" {
			jsFileFound = true
			break
		}
	}
	if jsFileFound {
		results <- struct {
			string
			bool
		}{standalone, true}
	}
	runtime.wg.Done()
}

func (runtime *nodejsRuntime) detectStandaloneGitlab(results chan struct {
	string
	bool
}, tree []*gitlab.TreeNode,
) {
	jsFileFound := false
	for i := 0; i < len(tree); i++ {
		name := tree[i].Name
		if name == "server.js" || name == "app.js" || name == "main.js" || name == "index.js" {
			jsFileFound = true
			break
		}
	}
	if jsFileFound {
		results <- struct {
			string
			bool
		}{standalone, true}
	}
	runtime.wg.Done()
}

// copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
func validateNvmrc(content string) (string, error) {
	content = strings.TrimSpace(strings.ToLower(content))

	if content == "lts/*" || content == "node" {
		return content, nil
	}

	for key := range lts {
		if content == strings.ToLower("lts/"+key) {
			return content, nil
		}
	}

	content = strings.TrimPrefix(content, "v")

	if _, err := semver.NewConstraint(content); err != nil {
		return "", fmt.Errorf("invalid version constraint specified in .nvmrc: %q", content)
	}

	return content, nil
}

// copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
func formatNvmrcContent(version string) string {
	if version == "node" {
		return "*"
	}

	if strings.HasPrefix(version, "lts") {
		ltsName := strings.SplitN(version, "/", 2)[1]
		if ltsName == "*" {
			var maxVersion int
			for _, versionValue := range lts {
				if maxVersion < versionValue {
					maxVersion = versionValue
				}
			}

			return fmt.Sprintf("%d.*", maxVersion)
		}

		return fmt.Sprintf("%d.*", lts[ltsName])
	}

	return version
}

// copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
func validateNodeVersion(content string) (string, error) {
	content = strings.TrimSpace(strings.ToLower(content))

	content = strings.TrimPrefix(content, "v")

	if _, err := semver.NewConstraint(content); err != nil {
		return "", fmt.Errorf("invalid version constraint specified in .node-version: %q", content)
	}

	return content, nil
}

func (runtime *nodejsRuntime) DetectGithub(
	client *github.Client,
	directoryContent []*github.RepositoryContent,
	owner, name, path string,
	repoContentOptions github.RepositoryContentGetOptions,
	paketo, heroku *BuilderInfo,
) error {
	results := make(chan struct {
		string
		bool
	}, 3)

	runtime.wg.Add(3)
	go runtime.detectYarnGithub(results, directoryContent)
	go runtime.detectNPMGithub(results, directoryContent)
	go runtime.detectStandaloneGithub(results, directoryContent)
	runtime.wg.Wait()
	close(results)

	paketoBuildpackInfo := BuildpackInfo{
		Name:      "NodeJS",
		Buildpack: "gcr.io/paketo-buildpacks/nodejs",
	}
	herokuBuildpackInfo := BuildpackInfo{
		Name:      "NodeJS",
		Buildpack: "heroku/nodejs",
	}

	if len(results) == 0 {
		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
		return nil
	}

	foundYarn := false
	foundNPM := false
	foundStandalone := false
	for result := range results {
		if result.string == yarn {
			foundYarn = true
		} else if result.string == npm {
			foundNPM = true
		} else if result.string == standalone {
			foundStandalone = true
		}
	}

	if foundYarn || foundNPM {
		// it is safe to assume that the project contains a package.json
		fileContent, _, _, err := client.Repositories.GetContents(
			context.Background(),
			owner,
			name,
			fmt.Sprintf("%s/package.json", path),
			&repoContentOptions,
		)
		if err != nil {
			paketo.Others = append(paketo.Others, paketoBuildpackInfo)
			heroku.Others = append(heroku.Others, herokuBuildpackInfo)
			return fmt.Errorf("error fetching contents of package.json: %v", err)
		}
		var packageJSON struct {
			Scripts map[string]string `json:"scripts"`
			Engines struct {
				Node string `json:"node"`
			} `json:"engines"`
		}

		data, err := fileContent.GetContent()
		if err != nil {
			paketo.Others = append(paketo.Others, paketoBuildpackInfo)
			heroku.Others = append(heroku.Others, herokuBuildpackInfo)
			return fmt.Errorf("error calling GetContent() on package.json: %v", err)
		}
		err = json.NewDecoder(strings.NewReader(data)).Decode(&packageJSON)
		if err != nil {
			paketo.Others = append(paketo.Others, paketoBuildpackInfo)
			heroku.Others = append(heroku.Others, herokuBuildpackInfo)
			return fmt.Errorf("error decoding package.json contents to struct: %v", err)
		}

		if packageJSON.Engines.Node == "" {
			// we should now check for the node engine version in .nvmrc and then .node-version
			nvmrcFound := false
			nodeVersionFound := false
			for i := 0; i < len(directoryContent); i++ {
				name := directoryContent[i].GetName()
				if name == ".nvmrc" {
					nvmrcFound = true
				} else if name == ".node-version" {
					nodeVersionFound = true
				}
			}

			if nvmrcFound {
				// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
				fileContent, _, _, err = client.Repositories.GetContents(
					context.Background(),
					owner,
					name,
					fmt.Sprintf("%s/.nvmrc", path),
					&repoContentOptions,
				)
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error fetching contents of .nvmrc: %v", err)
				}
				data, err = fileContent.GetContent()
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error calling GetContent() on .nvmrc: %v", err)
				}
				nvmrcVersion, err := validateNvmrc(data)
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error validating .nvmrc: %v", err)
				}
				nvmrcVersion = formatNvmrcContent(nvmrcVersion)

				if nvmrcVersion != "*" {
					packageJSON.Engines.Node = data
				}
			}

			if packageJSON.Engines.Node == "" && nodeVersionFound {
				// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
				fileContent, _, _, err = client.Repositories.GetContents(
					context.Background(),
					owner,
					name,
					fmt.Sprintf("%s/.node-version", path),
					&repoContentOptions,
				)
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error fetching contents of .node-version: %v", err)
				}
				data, err = fileContent.GetContent()
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error calling GetContent() on .node-version: %v", err)
				}
				nodeVersion, err := validateNodeVersion(data)
				if err != nil {
					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
					return fmt.Errorf("error validating .node-version: %v", err)
				}
				if nodeVersion != "" {
					packageJSON.Engines.Node = nodeVersion
				}
			}
		}

		if packageJSON.Engines.Node == "" {
			// use the default node engine version from https://github.com/paketo-buildpacks/node-engine/blob/main/buildpack.toml
			packageJSON.Engines.Node = "16.*.*"
		}

		paketoBuildpackInfo.Config = make(map[string]interface{})
		paketoBuildpackInfo.Config["scripts"] = packageJSON.Scripts
		paketoBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
		paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)

		herokuBuildpackInfo.Config = make(map[string]interface{})
		herokuBuildpackInfo.Config["scripts"] = packageJSON.Scripts
		herokuBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
		heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
	} else if foundStandalone {
		paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
		heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
	}

	return nil
}

func (runtime *nodejsRuntime) DetectGitlab(
	client *gitlab.Client,
	tree []*gitlab.TreeNode,
	repoPath, path, ref string,
	paketo, heroku *BuilderInfo,
) error {
	results := make(chan struct {
		string
		bool
	}, 3)

	runtime.wg.Add(3)
	go runtime.detectYarnGitlab(results, tree)
	go runtime.detectNPMGitlab(results, tree)
	go runtime.detectStandaloneGitlab(results, tree)
	runtime.wg.Wait()
	close(results)

	paketoBuildpackInfo := BuildpackInfo{
		Name:      "NodeJS",
		Buildpack: "gcr.io/paketo-buildpacks/nodejs",
	}
	herokuBuildpackInfo := BuildpackInfo{
		Name:      "NodeJS",
		Buildpack: "heroku/nodejs",
	}

	if len(results) == 0 {
		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
		return nil
	}

	// foundYarn := false
	// foundNPM := false
	// foundStandalone := false
	// for result := range results {
	// 	if result.string == yarn {
	// 		foundYarn = true
	// 	} else if result.string == npm {
	// 		foundNPM = true
	// 	} else if result.string == standalone {
	// 		foundStandalone = true
	// 	}
	// }

	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)

	// if foundYarn || foundNPM {
	// 	// it is safe to assume that the project contains a package.json
	// 	fileContent, _, err := client.RepositoryFiles.GetRawFile(
	// 		fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/package.json", path),
	// 		&gitlab.GetRawFileOptions{
	// 			Ref: gitlab.String(ref),
	// 		},
	// 	)
	// 	if err != nil {
	// 		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 		return fmt.Errorf("error fetching contents of package.json: %v", err)
	// 	}
	// 	var packageJSON struct {
	// 		Scripts map[string]string `json:"scripts"`
	// 		Engines struct {
	// 			Node string `json:"node"`
	// 		} `json:"engines"`
	// 	}

	// 	data := string(fileContent)

	// 	err = json.NewDecoder(strings.NewReader(data)).Decode(&packageJSON)
	// 	if err != nil {
	// 		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 		return fmt.Errorf("error decoding package.json contents to struct: %v", err)
	// 	}

	// 	if packageJSON.Engines.Node == "" {
	// 		// we should now check for the node engine version in .nvmrc and then .node-version
	// 		nvmrcFound := false
	// 		nodeVersionFound := false
	// 		for i := 0; i < len(tree); i++ {
	// 			name := tree[i].Name
	// 			if name == ".nvmrc" {
	// 				nvmrcFound = true
	// 			} else if name == ".node-version" {
	// 				nodeVersionFound = true
	// 			}
	// 		}

	// 		if nvmrcFound {
	// 			// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
	// 			fileContent, _, err = client.RepositoryFiles.GetRawFile(
	// 				fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/.nvmrc", path),
	// 				&gitlab.GetRawFileOptions{
	// 					Ref: gitlab.String(ref),
	// 				},
	// 			)
	// 			if err != nil {
	// 				paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 				heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 				return fmt.Errorf("error fetching contents of .nvmrc: %v", err)
	// 			}
	// 			data = string(fileContent)

	// 			nvmrcVersion, err := validateNvmrc(data)
	// 			if err != nil {
	// 				paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 				heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 				return fmt.Errorf("error validating .nvmrc: %v", err)
	// 			}
	// 			nvmrcVersion = formatNvmrcContent(nvmrcVersion)

	// 			if nvmrcVersion != "*" {
	// 				packageJSON.Engines.Node = data
	// 			}
	// 		}

	// 		if packageJSON.Engines.Node == "" && nodeVersionFound {
	// 			// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
	// 			fileContent, _, err = client.RepositoryFiles.GetRawFile(
	// 				fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/.node-version", path),
	// 				&gitlab.GetRawFileOptions{
	// 					Ref: gitlab.String(ref),
	// 				},
	// 			)
	// 			if err != nil {
	// 				paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 				heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 				return fmt.Errorf("error fetching contents of .node-version: %v", err)
	// 			}

	// 			data = string(fileContent)

	// 			nodeVersion, err := validateNodeVersion(data)
	// 			if err != nil {
	// 				paketo.Others = append(paketo.Others, paketoBuildpackInfo)
	// 				heroku.Others = append(heroku.Others, herokuBuildpackInfo)
	// 				return fmt.Errorf("error validating .node-version: %v", err)
	// 			}
	// 			if nodeVersion != "" {
	// 				packageJSON.Engines.Node = nodeVersion
	// 			}
	// 		}
	// 	}

	// 	if packageJSON.Engines.Node == "" {
	// 		// use the default node engine version from https://github.com/paketo-buildpacks/node-engine/blob/main/buildpack.toml
	// 		packageJSON.Engines.Node = "16.*.*"
	// 	}

	// 	paketoBuildpackInfo.Config = make(map[string]interface{})
	// 	paketoBuildpackInfo.Config["scripts"] = packageJSON.Scripts
	// 	paketoBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
	// 	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)

	// 	herokuBuildpackInfo.Config = make(map[string]interface{})
	// 	herokuBuildpackInfo.Config["scripts"] = packageJSON.Scripts
	// 	herokuBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
	// 	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
	// } else if foundStandalone {
	// 	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
	// 	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
	// }

	return nil
}
