//go:build darwin

// sandbox_darwin.go v1.6

package sandbox

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"apigo.cc/gojs"
	"github.com/ssgo/log"
	"github.com/ssgo/u"
)

var defaultVolumes = []Volume{
	{Source: "/bin", ReadOnly: true},
	{Source: "/usr/bin", ReadOnly: true},
	{Source: "/usr/lib", ReadOnly: true},
	{Source: "/usr/share", ReadOnly: true},
	{Source: "/usr/local/bin", ReadOnly: true},
	{Source: "/usr/local/lib", ReadOnly: true},
	{Source: "/usr/local/share", ReadOnly: true},
	{Source: "/usr/local/include", ReadOnly: true},
	{Source: "/usr/local/Cellar", ReadOnly: true},
	{Source: "/private/var/folders"},            // 必须：存储临时模块缓存、Python .pyc 的地方
	{Source: "/System/Library", ReadOnly: true}, // 必须：系统级 Frameworks (OpenSSL, Foundation)
	{Source: "/dev/fd"},                         // 很多 shell 操作和 Python 的 subprocess 重定向强依赖它。
	{Source: "/dev/null"},
	{Source: "/dev/zero"},
	{Source: "/dev/stdout"},
	{Source: "/dev/stderr"},
	{Source: "/dev/random", ReadOnly: true},
	{Source: "/dev/urandom", ReadOnly: true},
}

func Start(cfg *Config) (*Sandbox, error) {
	s := &Sandbox{
		config:      cfg,
		mountedList: []string{},
		status:      "created",
		extra:       map[string]any{},
	}

	s.lock.Lock()
	defer s.lock.Unlock()
	if s.status != "created" {
		return s, gojs.Err("sandbox is already used")
	}

	s.id = getId()
	s.status = "starting"
	s.startTime = time.Now().Unix()

	// 1. 物理环境准备
	s.root = filepath.Join(pluginConfig.Root, s.id)
	os.MkdirAll(s.root, 0755)

	sandboxTmp := filepath.Join(os.TempDir(), s.id, ".tmp")
	os.MkdirAll(sandboxTmp, 0777)

	// 2. 优先级逻辑：WorkDir 处理
	if s.config.ProjectDir != "" {
		s.workDir = u.GetAbsFilename(s.config.ProjectDir)
		if s.config.WorkDir != "" {
			log.DefaultLogger.Warning("[Sandbox] both HostWorkDir and WorkDir provided. Using HostWorkDir.")
		}
	} else if s.config.WorkDir != "" {
		s.workDir = u.GetAbsFilename(s.config.WorkDir)
	} else {
		s.workDir = filepath.Join(s.root, ".workdir")
		os.MkdirAll(s.workDir, 0755)
		// log.DefaultLogger.Warning("[Sandbox] no WorkDir specified. Using isolated temporary directory as WorkDir.")
	}
	if !u.FileExists(s.workDir) {
		os.MkdirAll(s.workDir, 0755)
	}

	vs := NewVolumes()
	vs.Add(defaultVolumes...)
	vs.Add(s.config.Volumes...)
	vs.Add(Volume{Source: s.workDir}) // 添加工作目录

	// 3. 不支持功能警告
	if s.config.Limits.Cpu > 0 || s.config.Limits.Mem > 0 {
		log.DefaultLogger.Warning("[Sandbox] resource 'Limits' are currently not enforced on Darwin.")
	}

	// 4. 环境变量
	initEnv := map[string]string{
		"PATH":             "/usr/local/bin:/usr/bin:/bin",
		"LANG":             os.Getenv("LANG"),
		"HOME":             s.workDir,
		"TMPDIR":           sandboxTmp,
		"PYTHONUNBUFFERED": "1",
	}
	addEnv := func(k, v string) {
		initEnv[k] = os.Expand(v, func(varName string) string {
			return initEnv[varName]
		})
	}

	for k, v := range s.config.Envs {
		addEnv(k, v)
	}

	// 处理 runtime
	runtimeListLock.RLock()
	rt := runtimeList[s.config.Runtime.Language]
	runtimeListLock.RUnlock()
	if rt != nil {
		rtCfg := pluginConfig.Runtime[s.config.Runtime.Language]
		if rtCfg == nil {
			rtCfg = &RuntimeConfig{}
		}
		if rtCfg.Root == "" {
			rtCfg.Root = filepath.Join(pluginConfig.Root, s.config.Runtime.Language)
		}
		runtimePath := filepath.Join(rtCfg.Root, "runtime", s.config.Runtime.Version)
		venvPath := filepath.Join(rtCfg.Root, "venv", fmt.Sprintf("%s_%s", s.config.Runtime.Version, s.config.Runtime.Venv))
		if rtsbCfg, err := rt.Check(runtimePath, venvPath, s.config.ProjectDir, s.uid, s.gid, rtCfg); err == nil {
			if s.config.StartCmd == "" && rtsbCfg.StartCmd != "" {
				s.config.StartCmd = rtsbCfg.StartCmd
			}
			for k, v := range rtsbCfg.Envs {
				addEnv(k, v)
			}
			// 挂载运行时环境
			if u.FileExists(runtimePath) {
				vs.Add(Volume{Source: runtimePath, ReadOnly: true})
			}
			if u.FileExists(venvPath) {
				vs.Add(Volume{Source: venvPath, ReadOnly: false})
			}
		} else {
			s._cleanup()
			return s, err
		}
	} else {
		s._cleanup()
		return s, gojs.Err("runtime not found for language " + s.config.Runtime.Language)
	}

	if s.config.StartCmd == "" {
		s._cleanup()
		return s, gojs.Err("start cmd is empty")
	}

	// 5. 生成 Seatbelt 策略
	sbProfile := s.generateSeatbelt(vs)
	sbPath := filepath.Join(s.root, "sandbox.sb")
	os.WriteFile(sbPath, []byte(sbProfile), 0644)

	// 6. 启动命令
	s.cmd = exec.Command("sandbox-exec", append([]string{"-f", sbPath, s.config.StartCmd}, s.config.StartArgs...)...)
	s.cmd.Dir = s.workDir
	s.cmd.Env = make([]string, 0, len(initEnv))
	for k, v := range initEnv {
		s.cmd.Env = append(s.cmd.Env, fmt.Sprintf("%s=%s", k, v))
	}

	if fdOut, err := os.OpenFile(filepath.Join(s.workDir, "stdout.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0644); err == nil {
		s.lastLogFd = fdOut
		s.cmd.Stdout = fdOut
	} else {
		s.cmd.Stdout = os.Stdout
	}
	if fdErr, err := os.OpenFile(filepath.Join(s.workDir, "stderr.log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0644); err == nil {
		s.errorLogFd = fdErr
		s.cmd.Stderr = fdErr
	} else {
		s.cmd.Stderr = os.Stderr
	}

	if err := s.cmd.Start(); err != nil {
		log.DefaultLogger.Error("[Sandbox] failed to start process", "err", err.Error(), "cmd", s.config.StartCmd, "args", s.config.StartArgs)
		s.errorLogFd.WriteString(fmt.Sprintf("Failed to start process: %s\n", err.Error()))
		s._cleanup()
		return s, err
	}

	s.status = "running"
	s.pid = s.cmd.Process.Pid
	u.Save(filepath.Join(s.root, ".state.json"), State{
		Id:        s.id,
		Pid:       s.pid,
		StartTime: s.startTime,
		WorkDir:   s.workDir,
	})
	RegisterSandbox(s)
	return s, nil
}

func (s *Sandbox) generateSeatbelt(vs *Volumes) string {
	var sb strings.Builder
	// 默认拒绝所有，仅允许根路径读取（无其他路径访问权限）
	sb.WriteString("(version 1)\n(deny default)\n")
	sb.WriteString("(allow file-read* (literal \"/\"))\n")
	sb.WriteString("(allow file-read-metadata (literal \"/\"))\n") // 没有它 Python 无法完成路径溯源

	// 挂载卷授权
	for _, v := range vs.Get() {
		mode := "file-read*"
		if !v.ReadOnly {
			mode += " file-write*"
		}
		typ := "subpath"
		if fi := u.GetFileInfo(v.Source); fi != nil && !fi.IsDir {
			typ = "literal" // 非文件夹
		}
		sb.WriteString(fmt.Sprintf("(allow %s (%s \"%s\"))\n", mode, typ, v.Source))
		s.assertLog("Mount "+v.Source, nil, "mode", mode, "id", s.id, "pid", s.pid, "name", s.config.Name)
	}

	// 系统能力
	sb.WriteString("(allow file-read-metadata)\n")
	sb.WriteString("(allow sysctl-read)\n") // 能看到 CPU 型号、内核版本。Python 的 platform, multiprocessing.cpu_count() 需要它
	sb.WriteString("(allow process*)\n")
	sb.WriteString("(allow signal (target self))\n") // 允许向自己发送信号，无法像其他进程发送信号。
	sb.WriteString("(allow ipc-posix-shm*)\n")

	if len(s.config.Gpu.Devices) > 0 {
		sb.WriteString("(allow iokit-open)\n") // 开启GPU权限
		sb.WriteString("(allow iokit-open (iokit-user-client-class \"AppleGraphicsControlClient\"))\n")
		sb.WriteString("(allow iokit-open (iokit-user-client-class \"AGXDeviceUserClient\"))\n") // 针对 Apple Silicon
	}

	// 入站：按需开启监听
	for _, port := range s.config.Network.AllowListen {
		sb.WriteString(fmt.Sprintf("(allow network-bind (local ip \"*:%d\"))\n", port))
		sb.WriteString(fmt.Sprintf("(allow network-inbound (local ip \"*:%d\"))\n", port))
	}
	// 出站：按需开窗
	if s.config.Network.AllowInternet || s.config.Network.AllowLocalNetwork || len(s.config.Network.AllowList) > 0 {
		// 允许 DNS 和系统网络配置查询
		sb.WriteString("(allow mach-lookup (global-name \"com.apple.dnssd.service\"))\n")
		sb.WriteString("(allow mach-lookup (global-name \"com.apple.SystemConfiguration.configd\"))\n")
		if s.config.Network.AllowInternet {
			sb.WriteString("(allow network-outbound (remote ip \"*:*\"))\n")
		} else if s.config.Network.AllowLocalNetwork {
			// 因为无法精细控制，本地网络指 localhost 而非局域网
			sb.WriteString("(allow network* (remote ip \"localhost:*\"))\n")
		}
	}
	// 允许白名单访问
	for _, ipport := range s.config.Network.AllowList {
		// 受 Seatbelt 限制，不支持指定IP，仅支持限制端口
		if a := strings.Split(ipport, ":"); len(a) >= 2 {
			sb.WriteString(fmt.Sprintf("(allow network-outbound (remote ip \"*:%s\"))\n", a[len(a)-1]))
		}
	}
	// 拒绝白名单访问
	for _, ipport := range s.config.Network.BlockList {
		// 受 Seatbelt 限制，不支持指定IP，仅限制端口
		if a := strings.Split(ipport, ":"); len(a) >= 2 {
			sb.WriteString(fmt.Sprintf("(deny network-outbound (remote ip \"*:%s\"))\n", a[len(a)-1]))
		}
	}

	// 以下为“按需放行”的可选能力，默认注释以保安全
	// 例如传入 "(allow mach-per-user-lookup)" // 仅当涉及复杂系统 UI 服务时开启，能访问剪切板、用户偏好设置等系统服务因此风险较高，默认关闭
	for _, o := range s.config.ExtraOptions {
		if (strings.HasPrefix(o, "(allow ") || strings.HasPrefix(o, "(deny ")) && strings.HasSuffix(o, ")") {
			sb.WriteString(o + "\n")
		}
	}

	// fmt.Println(" ====Seatbelt:", sb.String())
	return sb.String()
}

func (s *Sandbox) Kill() error {
	s.lock.Lock()
	defer s.lock.Unlock()
	if s.status != "running" && s.status != "starting" {
		return gojs.Err("sandbox not running or starting")
	}
	s.status = "stopping"
	return s._kill(syscall.SIGTERM)
}

func (s *Sandbox) Wait(timeout int64) (any, error) {
	s.lock.Lock()
	defer s.lock.Unlock()
	if s.status != "running" && s.status != "stopping" {
		return nil, gojs.Err("sandbox not running or stopping")
	}

	if s._alive() {
		ch := make(chan error, 1)
		go func() {
			state, err := s._waitProcess()
			if err == nil && state != nil {
				s.status = "exited"
			}
			ch <- err
		}()

		select {
		case _ = <-ch:
			break
		case <-time.After(time.Duration(timeout) * time.Millisecond):
			s._kill(syscall.SIGKILL)
			break
		}
	}

	return s._cleanup()
}

func (s *Sandbox) Cleanup() (any, error) {
	s.lock.Lock()
	defer s.lock.Unlock()
	return s._cleanup()
}

func (s *Sandbox) _cleanup() (any, error) {
	s.log("cleaning up sandbox", "id", s.id, "name", s.config.Name)
	if s.lastLogFd != nil {
		s.lastLogFd.Close()
		s.lastLogFd = nil
	}
	if s.errorLogFd != nil {
		s.errorLogFd.Close()
		s.errorLogFd = nil
	}

	// 存储日志（按起始天合并）
	_, err := copyLog(s.workDir, s.startTime)
	s.assertLog("Copy log to logs directory", err, "id", s.id, "name", s.config.Name, "workDir", s.workDir)
	outLog := u.ReadFileN(filepath.Join(s.workDir, "stdout.log"))
	errLog := u.ReadFileN(filepath.Join(s.workDir, "stderr.log"))

	err = os.RemoveAll(s.root)
	s.assertLog(fmt.Sprintf("Remove Sandbox %s", s.root), err, "id", s.id, "name", s.config.Name)

	releaseId(s.id)
	ReleaseSandbox(s.id)

	var outData any = outLog
	if strings.HasPrefix(outLog, "{") && strings.HasSuffix(outLog, "}") || strings.HasPrefix(outLog, "[") && strings.HasSuffix(outLog, "]") {
		var data map[string]any
		if err := json.Unmarshal([]byte(outLog), &data); err == nil {
			outData = data
		}
	}

	if errLog != "" {
		return outData, errors.New(errLog)
	}
	return outData, nil
}

func (s *Sandbox) Status() Status {
	s.lock.Lock()
	defer s.lock.Unlock()
	st := Status{Id: s.id, Pid: s.pid, Status: s.status, StartTime: s.startTime, Uptime: time.Now().Unix() - s.startTime}
	st.Alive = s._alive()
	return st
}
