package sandbox

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

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

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

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

func checkPythonRuntime(runtimePath string, cfg *RuntimeConfig) error {
	runtimeVersion := filepath.Base(runtimePath)
	lock := getRuntimeLock("python", "runtime", runtimePath)
	lock.Lock()
	defer lock.Unlock()
	if !u.FileExists(runtimePath) {
		osPart := u.StringIf(runtime.GOOS == "linux", "unknown-linux-gnu", u.StringIf(runtime.GOOS == "windows", "pc-windows-msvc-shared", "apple-darwin"))
		archPart := u.StringIf(runtime.GOARCH == "arm64", "aarch64", "x86_64")
		instFile := filepath.Join(filepath.Dir(runtimePath), ".installs", fmt.Sprintf("%s-%s-%s.tar.gz", runtimeVersion, osPart, archPart))
		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 python runtime release list", "from", "https://github.com/astral-sh/python-build-standalone/releases", "httpProxy", cfg.HttpProxy)
			r1 := downloadClient.Get("https://github.com/astral-sh/python-build-standalone/releases")
			if r1.Error != nil {
				return r1.Error
			}
			if m1 := regexp.MustCompile(`data-deferred-src="([^"]+)"`).FindStringSubmatch(r1.String()); len(m1) >= 2 {
				log.DefaultLogger.Info("[Sandbox] fetching python runtime download url", "from", m1[1], "httpProxy", cfg.HttpProxy)
				r2 := downloadClient.Get(m1[1])
				if r2.Error != nil {
					return r2.Error
				}
				pattern := fmt.Sprintf(`href="([^"]+cpython-%s(?:\.\d+)*\+[^"]+-%s-%s-install_only\.tar\.gz)"`, regexp.QuoteMeta(runtimeVersion), archPart, osPart)
				if m2 := regexp.MustCompile(pattern).FindStringSubmatch(r2.String()); len(m2) >= 2 {
					log.DefaultLogger.Info("[Sandbox] downloading python runtime", "from", "https://github.com"+m2[1], "to", instFile)
					if _, err := downloadClient.Download(instFile, "https://github.com"+m2[1], nil); err != nil {
						return err
					} else {
						log.DefaultLogger.Info("[Sandbox] downloaded python runtime", "version", runtimeVersion, "os", osPart, "arch", archPart, "url", "https://github.com"+m2[1], "to", instFile, "httpProxy", cfg.HttpProxy)
					}
				} else {
					return fmt.Errorf("no binary for version %s on %s-%s on %s", runtimeVersion, osPart, archPart, m1[1])
				}
			} else {
				return errors.New("can't read release list on https://github.com/astral-sh/python-build-standalone/releases")
			}
		}

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

func checkPythonVenv(runtimePath, venvPath string, uid, gid int) error {
	lock := getRuntimeLock("python", "venv", venvPath)
	lock.Lock()
	defer lock.Unlock()
	if !u.FileExists(venvPath) {
		pyExe := getPythonExe(runtimePath)
		os.MkdirAll(venvPath, 0755)
		if uid != 0 && runtime.GOOS == "linux" {
			os.Chown(venvPath, uid, gid)
		}
		if out, err := RunCmd(venvPath, uid, gid, pyExe, "-m", "venv", venvPath); err != nil {
			log.DefaultLogger.Error("[Sandbox] venv create failed", "runtime", runtimePath, "venv", venvPath, "err", err.Error(), "output", out)
			return err
		} else {
			log.DefaultLogger.Info("[Sandbox] venv create success", "runtime", runtimePath, "venv", venvPath)
		}
	}
	return nil
}

func checkPythonProject(venvPath, projectPath string, uid, gid int, cfg *RuntimeConfig) error {
	lock := getRuntimeLock("python", "project", projectPath)
	lock.Lock()
	defer lock.Unlock()
	if uid != 0 && runtime.GOOS == "linux" {
		os.Chown(projectPath, uid, gid)
	}
	reqFile := filepath.Join(projectPath, "requirements.txt")
	if u.FileExists(reqFile) {
		shaFile := filepath.Join(projectPath, ".requirements.sha1")
		currentSha := u.Sha1String(u.ReadFileN(reqFile))
		oldSha := u.ReadFileN(shaFile)
		if oldSha != currentSha {
			pipExe := getPipExe(venvPath)
			pipArgs := []string{"install", "-r", reqFile}
			if cfg.Mirror != "" {
				pipArgs = append(pipArgs, "-i", cfg.Mirror)
			}

			if out, err := RunCmd(projectPath, uid, gid, pipExe, pipArgs...); err != nil {
				log.DefaultLogger.Error("[Sandbox] pip install failed", "venv", venvPath, "project", projectPath, "err", err.Error(), "output", out)
				return err
			} else {
				log.DefaultLogger.Info("[Sandbox] pip install success", "venv", venvPath, "project", projectPath, "output", out)
				u.WriteFile(shaFile, currentSha)
			}
		}
	}
	return nil
}

func getPythonExe(base string) string {
	if runtime.GOOS == "windows" {
		return filepath.Join(base, "python.exe")
	}
	exe := filepath.Join(base, "bin", "python")
	if !u.FileExists(exe) {
		exe = filepath.Join(base, "bin", "python3")
	}
	return exe
}

func getPipExe(base string) string {
	if runtime.GOOS == "windows" {
		return filepath.Join(base, "pip.exe")
	}
	exe := filepath.Join(base, "bin", "pip")
	if !u.FileExists(exe) {
		exe = filepath.Join(base, "bin", "pip3")
	}
	return exe
}
