//go:build !windows
// +build !windows

// Author: NiuJiuRu
// Email: niujiuru@qq.com

package shell

import (
	"os"
	"path/filepath"
	"strings"
	"syscall"
)

type Executor struct {
	cwd     string        // 当前目录
	prevCwd string        // 上次目录
	pg      *processGroup // 执行命令的进程组
}

type PathError struct {
	Path string
	Info string
}

func (e *PathError) Error() string {
	return e.Info + ": " + e.Path
}

func NewExecutor() *Executor {
	return &Executor{cwd: "/"}
}

func (e *Executor) Exec(p ExecuteParams) (*ExecuteResult, error) {
	if e.isCD(p.Cmd) {
		dir, err := e.resolveCD(e.cwd, p.Cmd)
		if err != nil {
			return &ExecuteResult{
				Stderr:   err.Error(),
				ExitCode: 1,
			}, nil
		}
		e.prevCwd = e.cwd
		e.cwd = dir
		return &ExecuteResult{ExitCode: 0, Cwd: e.cwd}, nil
	}

	if strings.TrimSpace(p.Cmd) == "pwd" {
		return &ExecuteResult{
			Stdout:   e.cwd + "\n",
			ExitCode: 0,
		}, nil
	}

	p.Dir = e.cwd

	defer func() {
		e.pg = nil // 命令结束
	}()

	result, err := executeInternal(p, func(pg *processGroup) { e.pg = pg })
	if result != nil {
		result.Cwd = e.cwd
	}

	return result, err
}

func (e *Executor) isCD(cmd string) bool {
	s := strings.TrimSpace(cmd)
	return s == "cd" || strings.HasPrefix(s, "cd ")
}

func (e *Executor) resolveCD(cwd, cmd string) (string, error) {
	fields := strings.Fields(cmd)
	if len(fields) == 1 { // 只有"cd", 没有参数
		return "/", nil
	}

	path := fields[1]
	switch path {
	case "~":
		home := os.Getenv("HOME")
		if home == "" {
			home = "/"
		}
		return home, nil
	case "-":
		if e.prevCwd == "" {
			return cwd, nil
		}
		return e.prevCwd, nil
	}

	var target string
	if filepath.IsAbs(path) { // 绝对路径
		target = filepath.Clean(path)
	} else { // 相对路径
		target = filepath.Clean(filepath.Join(cwd, path))
	}

	info, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return "", &PathError{Path: target, Info: "directory not exist"}
		}
		return "", err
	}
	if !info.IsDir() {
		return "", &PathError{Path: target, Info: "not a directory"}
	}

	return target, nil
}

func (e *Executor) Interrupt() error {
	if e.pg == nil {
		return nil
	}

	return syscall.Kill(-e.pg.pgid, syscall.SIGINT) // 等价于 Ctrl+C
}
