package sandbox

import (
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/ssgo/httpclient"
	"github.com/ssgo/log"
	"github.com/ssgo/u"
)

func init() {
	RegisterRuntime("nodejs", &Runtime{
		Check: func(runtimePath, venvPath, projectPath string, uid, gid int, cfg *RuntimeConfig) (*RuntimeSandboxConfig, error) {
			if err := checkNodejsRuntime(runtimePath, cfg); err != nil {
				return nil, err
			}
			if err := checkNodejsProject(runtimePath, projectPath, uid, gid, cfg); err != nil {
				return nil, err
			}

			nodeExe := getNodeExe(runtimePath)
			return &RuntimeSandboxConfig{
				StartCmd: nodeExe,
				Envs: map[string]string{
					"PATH": filepath.Dir(nodeExe) + string(os.PathListSeparator) + "$PATH",
				},
			}, nil
		},
	})
}

func checkNodejsRuntime(runtimePath string, cfg *RuntimeConfig) error {
	runtimeVersion := filepath.Base(runtimePath)
	lock := getRuntimeLock("nodejs", "runtime", runtimePath)
	lock.Lock()
	defer lock.Unlock()
	if !u.FileExists(runtimePath) {
		osPart := u.StringIf(runtime.GOOS == "linux", "linux", u.StringIf(runtime.GOOS == "windows", "win", "darwin"))
		archPart := u.StringIf(runtime.GOARCH == "arm64", "arm64", "x64")
		extension := u.StringIf(runtime.GOOS == "windows", ".zip", ".tar.gz")
		instFile := filepath.Join(filepath.Dir(runtimePath), ".installs", fmt.Sprintf("node-v%s-%s-%s%s", runtimeVersion, osPart, archPart, extension))
		if !u.FileExists(instFile) {
			downloadClient := httpclient.GetClient(3600 * time.Second)
			downloadClient.EnableRedirect()
			if cfg.HttpProxy != "" {
				if proxyURL, err := url.Parse(cfg.HttpProxy); err == nil {
					downloadClient.GetRawClient().Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
				}
			}
			log.DefaultLogger.Info("[Sandbox] fetching nodejs runtime release list", "from", "https://nodejs.org/dist/index.json", "httpProxy", cfg.HttpProxy)
			r1 := downloadClient.Get("https://nodejs.org/dist/index.json")
			if r1.Error != nil {
				return r1.Error
			}

			// 解析JSON数据
			var versions []map[string]interface{}
			if err := r1.To(&versions); err != nil {
				return fmt.Errorf("failed to parse index.json: %v", err)
			}

			// 寻找匹配的版本
			var targetVersion string
			for _, v := range versions {
				version := v["version"].(string)
				// 检查版本号是否匹配
				if strings.HasPrefix(version, "v"+runtimeVersion) {
					// 检查是否有对应的文件
					files := v["files"].([]interface{})
					fileFound := false
					for _, f := range files {
						file := f.(string)
						// 检查是否有对应的平台和架构的文件
						if (osPart == "linux" && file == "linux-"+archPart) ||
							(osPart == "darwin" && (file == "osx-"+archPart+"-tar" || file == "osx-x64-tar")) ||
							(osPart == "win" && file == "win-"+archPart+"-zip") {
							fileFound = true
							break
						}
					}
					if fileFound {
						targetVersion = version
						break
					}
				}
			}

			if targetVersion == "" {
				return fmt.Errorf("no binary for version %s on %s-%s", runtimeVersion, osPart, archPart)
			}

			// 构建下载URL
			fileName := fmt.Sprintf("node-%s-%s-%s%s", targetVersion, osPart, archPart, extension)
			downloadURL := fmt.Sprintf("https://nodejs.org/dist/%s/%s", targetVersion, fileName)

			log.DefaultLogger.Info("[Sandbox] downloading nodejs runtime", "from", downloadURL, "to", instFile, "httpProxy", cfg.HttpProxy)
			if _, err := downloadClient.Download(instFile, downloadURL, nil); err != nil {
				return err
			} else {
				log.DefaultLogger.Info("[Sandbox] downloaded nodejs runtime", "version", targetVersion, "os", osPart, "arch", archPart, "url", downloadURL, "to", instFile)
			}
		}

		os.MkdirAll(runtimePath, 0755)
		if err := u.Extract(instFile, runtimePath, true); err != nil {
			log.DefaultLogger.Error("[Sandbox] unpack nodejs runtime failed", "version", runtimeVersion, "os", osPart, "arch", archPart, "from", instFile, "err", err.Error())
			return err
		}
	}
	return nil
}

func checkNodejsProject(runtimePath, projectPath string, uid, gid int, cfg *RuntimeConfig) error {
	lock := getRuntimeLock("nodejs", "project", projectPath)
	lock.Lock()
	defer lock.Unlock()
	if uid != 0 && runtime.GOOS == "linux" {
		os.Chown(projectPath, uid, gid)
	}
	packageFile := filepath.Join(projectPath, "package.json")
	if u.FileExists(packageFile) {
		shaFile := filepath.Join(projectPath, ".package.sha1")
		currentSha := u.Sha1String(u.ReadFileN(packageFile))
		oldSha := u.ReadFileN(shaFile)
		if oldSha != currentSha {
			npmExe := getNpmExe(runtimePath)
			npmArgs := []string{"install", "--prefix", projectPath, "--cache", filepath.Join(projectPath, ".npm")}
			if cfg.Mirror != "" {
				npmArgs = append(npmArgs, "--registry", cfg.Mirror)
			}

			if out, err := RunCmdWithEnv(map[string]string{"PATH": filepath.Dir(npmExe) + string(os.PathListSeparator) + os.Getenv("PATH")}, projectPath, uid, gid, npmExe, npmArgs...); err != nil {
				log.DefaultLogger.Error("[Sandbox] npm install failed", "runtime", runtimePath, "project", projectPath, "err", err.Error(), "output", out)
				return err
			} else {
				log.DefaultLogger.Info("[Sandbox] npm install success", "runtime", runtimePath, "project", projectPath, "output", out)
				u.WriteFile(shaFile, currentSha)
			}
		}
	}
	return nil
}

func getNodeExe(base string) string {
	if runtime.GOOS == "windows" {
		return filepath.Join(base, "node.exe")
	}
	return filepath.Join(base, "bin", "node")
}

func getNpmExe(base string) string {
	if runtime.GOOS == "windows" {
		return filepath.Join(base, "npm.cmd")
	}
	return filepath.Join(base, "bin", "npm")
}
