package yarninstall

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/paketo-buildpacks/libnodejs"
	"github.com/paketo-buildpacks/packit/v2"
	"github.com/paketo-buildpacks/packit/v2/chronos"
	"github.com/paketo-buildpacks/packit/v2/sbom"
	"github.com/paketo-buildpacks/packit/v2/scribe"
)

//go:generate faux --interface SymlinkManager --output fakes/symlink_manager.go
type SymlinkManager interface {
	Link(oldname, newname string) error
	Unlink(path string) error
}

//go:generate faux --interface InstallProcess --output fakes/install_process.go
type InstallProcess interface {
	ShouldRun(workingDir string, metadata map[string]interface{}) (run bool, sha string, err error)
	SetupModules(workingDir, currentModulesLayerPath, nextModulesLayerPath string) (string, error)
	Execute(workingDir, modulesLayerPath string, launch bool) error
}

//go:generate faux --interface EntryResolver --output fakes/entry_resolver.go
type EntryResolver interface {
	MergeLayerTypes(string, []packit.BuildpackPlanEntry) (launch, build bool)
}

//go:generate faux --interface SBOMGenerator --output fakes/sbom_generator.go
type SBOMGenerator interface {
	Generate(dir string) (sbom.SBOM, error)
}

//go:generate faux --interface ConfigurationManager --output fakes/configuration_manager.go
type ConfigurationManager interface {
	DeterminePath(typ, platformDir, entry string) (path string, err error)
}

func Build( entryResolver EntryResolver,
	configurationManager ConfigurationManager,
	homeDir string,
	symlinker SymlinkManager,
	installProcess InstallProcess,
	sbomGenerator SBOMGenerator,
	clock chronos.Clock,
	logger scribe.Emitter,
	tmpDir string) packit.BuildFunc {
	return func(context packit.BuildContext) (packit.BuildResult, error) {
		logger.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version)

		projectPath, err := libnodejs.FindProjectPath(context.WorkingDir)
		if err != nil {
			return packit.BuildResult{}, err
		}

		// Delete existing node_modules if present
		err = os.RemoveAll(filepath.Join(projectPath, "node_modules"))
		if err != nil {
				fmt.Errorf("failed to remove node_modules symlink: %w", err)
		}

		globalNpmrcPath, err := configurationManager.DeterminePath("npmrc", context.Platform.Path, ".npmrc")
		if err != nil {
			return packit.BuildResult{}, err
		}

		if globalNpmrcPath != "" {
			err = symlinker.Link(globalNpmrcPath, filepath.Join(homeDir, ".npmrc"))
			if err != nil {
				return packit.BuildResult{}, err
			}
		}

		globalYarnrcPath, err := configurationManager.DeterminePath("yarnrc", context.Platform.Path, ".yarnrc")
		if err != nil {
			return packit.BuildResult{}, err
		}

		if globalYarnrcPath != "" {
			err = symlinker.Link(globalYarnrcPath, filepath.Join(homeDir, ".yarnrc"))
			if err != nil {
				return packit.BuildResult{}, err
			}
		}

		launch, build := entryResolver.MergeLayerTypes(PlanDependencyNodeModules, context.Plan.Entries)

		sbomDisabled, err := checkSbomDisabled()
		if err != nil {
			return packit.BuildResult{}, err
		}

		var layers []packit.Layer
		var currentModLayer string
		if build {
			layer, err := context.Layers.Get("build-modules")
			if err != nil {
				return packit.BuildResult{}, err
			}

			logger.Process("Resolving installation process")

			run, sha, err := installProcess.ShouldRun(projectPath, layer.Metadata)
			if err != nil {
				return packit.BuildResult{}, err
			}

			if run {
				logger.Subprocess("Selected default build process: 'yarn install'")
				logger.Break()
				logger.Process("Executing build environment install process")

				layer, err = layer.Reset()
				if err != nil {
					return packit.BuildResult{}, err
				}

				currentModLayer, err = installProcess.SetupModules(projectPath, currentModLayer, layer.Path)
				if err != nil {
					return packit.BuildResult{}, err
				}

				duration, err := clock.Measure(func() error {
					return installProcess.Execute(projectPath, layer.Path, false)
				})
				if err != nil {
					return packit.BuildResult{}, err
				}

				logger.Action("Completed in %s", duration.Round(time.Millisecond))
				logger.Break()

				layer.Metadata = map[string]interface{}{
					"cache_sha": sha,
				}

				// err = ensureNodeModulesSymlink(projectPath, layer.Path, tmpDir)
				// if err != nil {
				// 	return packit.BuildResult{}, err
				// }

				path := filepath.Join(layer.Path, "node_modules", ".bin")
				layer.BuildEnv.Append("PATH", path, string(os.PathListSeparator))
				layer.BuildEnv.Override("NODE_ENV", "development")

				logger.EnvironmentVariables(layer)

				if sbomDisabled {
					logger.Subprocess("Skipping SBOM generation for Yarn Install")
					logger.Break()

				} else {
					logger.GeneratingSBOM(layer.Path)
					var sbomContent sbom.SBOM
					duration, err = clock.Measure(func() error {
						sbomContent, err = sbomGenerator.Generate(context.WorkingDir)
						return err
					})
					if err != nil {
						return packit.BuildResult{}, err
					}
					logger.Action("Completed in %s", duration.Round(time.Millisecond))
					logger.Break()

					logger.FormattingSBOM(context.BuildpackInfo.SBOMFormats...)
					layer.SBOM, err = sbomContent.InFormats(context.BuildpackInfo.SBOMFormats...)
					if err != nil {
						return packit.BuildResult{}, err
					}
				}
			} else {
				logger.Process("Reusing cached layer %s", layer.Path)

				currentModLayer, err = installProcess.SetupModules(projectPath, layer.Path, projectPath)
				if err != nil {
					return packit.BuildResult{}, err
				}
				// err = ensureNodeModulesSymlink(projectPath, layer.Path, tmpDir)
				// if err != nil {
				// 	return packit.BuildResult{}, err
				// }
			}

			layer.Build = true
			layer.Cache = true

			layers = append(layers, layer)
		}

		if launch {
			layer, err := context.Layers.Get("launch-modules")
			if err != nil {
				return packit.BuildResult{}, err
			}

			logger.Process("Resolving installation process")

			run, sha, err := installProcess.ShouldRun(projectPath, layer.Metadata)
			if err != nil {
				return packit.BuildResult{}, err
			}

			if run {
				logger.Subprocess("Selected default build process: 'yarn install'")
				logger.Break()
				logger.Process("Executing launch environment install process")
				fmt.Println("SHA:", sha)

				layer, err = layer.Reset()
				if err != nil {
					return packit.BuildResult{}, err
				}

				// _, err = installProcess.SetupModules(projectPath, currentModLayer, layer.Path)
				// if err != nil {
				// 	return packit.BuildResult{}, err
				// }

				// duration, err := clock.Measure(func() error {
				// 	return installProcess.Execute(projectPath, layer.Path, true)
				// })
				// if err != nil {
				// 	return packit.BuildResult{}, err
				// }

				// logger.Action("Completed in %s", duration.Round(time.Millisecond))
				// logger.Break()

				// if !build {
				// 	err = ensureNodeModulesSymlink(projectPath, layer.Path, tmpDir)
				// 	if err != nil {
				// 		return packit.BuildResult{}, err
				// 	}
				// }

				// layer.Metadata = map[string]interface{}{
				// 	"cache_sha": sha,
				// }

				path := filepath.Join(projectPath, "node_modules", ".bin")
				layer.LaunchEnv.Append("PATH", path, string(os.PathListSeparator))
				layer.LaunchEnv.Default("NODE_PROJECT_PATH", projectPath)

				logger.EnvironmentVariables(layer)

				// if sbomDisabled {
				// 	logger.Subprocess("Skipping SBOM generation for Yarn Install")
				// 	logger.Break()

				// } else {
				// 	logger.GeneratingSBOM(layer.Path)
				// 	var sbomContent sbom.SBOM
				// 	duration, err = clock.Measure(func() error {
				// 		sbomContent, err = sbomGenerator.Generate(context.WorkingDir)
				// 		return err
				// 	})
				// 	if err != nil {
				// 		return packit.BuildResult{}, err
				// 	}
				// 	logger.Action("Completed in %s", duration.Round(time.Millisecond))
				// 	logger.Break()

				// 	logger.FormattingSBOM(context.BuildpackInfo.SBOMFormats...)
				// 	layer.SBOM, err = sbomContent.InFormats(context.BuildpackInfo.SBOMFormats...)
				// 	if err != nil {
				// 		return packit.BuildResult{}, err
				// 	}
				// }

				// layer.ExecD = []string{filepath.Join(context.CNBPath, "bin", "setup-symlinks")}

			} else {
				logger.Process("Reusing cached layer %s", layer.Path)
				// if !build {
				// 	err = ensureNodeModulesSymlink(projectPath, layer.Path, tmpDir)
				// 	if err != nil {
				// 		return packit.BuildResult{}, err
				// 	}
				// }
			}

			layer.Launch = true

			layers = append(layers, layer)

		}

		err = symlinker.Unlink(filepath.Join(homeDir, ".npmrc"))
		if err != nil {
			return packit.BuildResult{}, err
		}

		err = symlinker.Unlink(filepath.Join(homeDir, ".yarnrc"))
		if err != nil {
			return packit.BuildResult{}, err
		}

		return packit.BuildResult{
			Layers: layers,
		}, nil
	}
}

func checkSbomDisabled() (bool, error) {
	if disableStr, ok := os.LookupEnv("BP_DISABLE_SBOM"); ok {
		disable, err := strconv.ParseBool(disableStr)
		if err != nil {
			return false, fmt.Errorf("failed to parse BP_DISABLE_SBOM value %s: %w", disableStr, err)
		}
		return disable, nil
	}
	return false, nil
}

func ensureNodeModulesSymlink(projectDir, targetLayer, tmpDir string) error {
	projectDirNodeModules := filepath.Join(projectDir, "node_modules")
	layerNodeModules := filepath.Join(targetLayer, "node_modules")
	tmpNodeModules := filepath.Join(tmpDir, "node_modules")

	for _, d := range []string{projectDirNodeModules, tmpNodeModules} {
		err := os.RemoveAll(d)
		if err != nil {
			return err
		}
	}

	err := os.Symlink(tmpNodeModules, projectDirNodeModules)
	if err != nil {
		return err
	}

	err = os.Symlink(layerNodeModules, tmpNodeModules)
	if err != nil {
		return err
	}

	return nil
}