package utils

import (
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	NIS_YPCAT  = "ypcat"
	SYS_PASSWD = "/etc/passwd"
)

// LinuxSysUser linux系统用户信息
type LinuxSysUser struct {
	Name       string
	Home       string
	Shell      string
	Uid        int
	Gid        int
	SSHkeyInfo map[string]SSHPubKeyInfo
}

func GetOneSysUser(name string) (*LinuxSysUser, error) {
	content, err := os.ReadFile(SYS_PASSWD)
	if err != nil {
		return nil, err
	}
	lines := strings.Split(strings.Trim(string(content), "\n"), "\n")
	for _, line := range lines {
		if strings.HasPrefix(line, name) {
			fields := strings.Split(line, ":")
			if len(fields) != 7 || fields[0] != name {
				continue
			}
			uid, err := strconv.Atoi(fields[2])
			if err != nil {
				continue
			}
			if uid < 999 && uid != 0 {
				continue
			}
			u := LinuxSysUser{}
			u.Uid = uid
			gid, err := strconv.Atoi(fields[3])
			if err != nil {
				continue
			}
			u.Gid = gid
			u.Name = fields[0]
			u.Shell = fields[6]
			u.Home = fields[5]
			s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
			if err == nil {
				u.SSHkeyInfo = s
			}
			return &u, nil
		}
	}
	return nil, nil
}

func GetOneNisUser(name string) (*LinuxSysUser, error) {
	ypcat := FindCmd(NIS_YPCAT)
	if ypcat == nil {
		return nil, nil
	}
	content, err := exec.Command(NIS_YPCAT, "passwd").Output()
	if err != nil {
		return nil, err
	}
	lines := strings.Split(strings.Trim(string(content), "\n"), "\n")
	for _, line := range lines {
		if strings.HasPrefix(line, name) {
			fields := strings.Split(line, ":")
			if len(fields) != 7 || fields[0] != name {
				continue
			}
			uid, err := strconv.Atoi(fields[2])
			if err != nil {
				continue
			}
			if uid < 999 && uid != 0 {
				continue
			}
			u := LinuxSysUser{}
			u.Uid = uid
			gid, err := strconv.Atoi(fields[3])
			if err != nil {
				continue
			}
			u.Gid = gid
			u.Name = fields[0]
			u.Shell = fields[6]
			u.Home = fields[5]
			s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
			if err == nil {
				u.SSHkeyInfo = s
			}
			return &u, nil
		}
	}
	return nil, nil
}

// GetSysUser 获取系统用户信息
func GetSysUser() (map[int]LinuxSysUser, error) {
	content, err := os.ReadFile(SYS_PASSWD)
	if err != nil {
		return nil, err
	}
	r := parsePasswd(string(content))
	return r, nil
}

// GetNisUser 获取Nis用户信息
func GetNisUser() (map[int]LinuxSysUser, error) {
	ypcat := FindCmd(NIS_YPCAT)
	if ypcat == nil {
		return make(map[int]LinuxSysUser), nil
	}
	output, err := exec.Command(NIS_YPCAT, "passwd").Output()
	if err != nil {
		return nil, err
	}
	r := parsePasswd(string(output))
	return r, nil
}

// GetClusUser 获取Clusconf用户信息
func GetClusUser() {
	// todo
}

type SSHPubKeyInfo struct {
	Type     string
	Content  string // base64格式的公钥
	UserInfo string
	Sha256   string // 公钥经过sha256后，再base64，如果结尾是=，删除=
}

// ParseSSHAuthKey 解析ssh认证文件的内容
func ParseSSHAuthKey(path string) (map[string]SSHPubKeyInfo, error) {
	content, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	pub := string(content)
	lines := strings.Split(strings.Trim(pub, "\n"), "\n")
	result := make(map[string]SSHPubKeyInfo)
	for _, line := range lines {
		if len(line) == 0 {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) != 3 {
			continue
		}
		if !strings.HasPrefix(fields[0], "ssh-") {
			continue
		}
		item := SSHPubKeyInfo{}
		item.Type, _ = strings.CutPrefix(fields[0], "ssh-")
		item.Content = fields[1]
		item.UserInfo = fields[2]
		key, err := base64.StdEncoding.DecodeString(item.Content)
		if err != nil {
			continue
		}
		b := sha256.Sum256(key)
		item.Sha256 = strings.Trim(base64.StdEncoding.EncodeToString(b[:]), "=")
		result[item.Sha256] = item
	}
	return result, nil
}

func parsePasswd(str string) map[int]LinuxSysUser {
	result := make(map[int]LinuxSysUser)
	pwd := strings.Trim(string(str), "\n")
	lines := strings.Split(pwd, "\n")
	for _, line := range lines {
		fields := strings.Split(line, ":")
		if len(fields) != 7 {
			continue
		}

		uid, err := strconv.Atoi(fields[2])
		if err != nil {
			continue
		}
		if uid < 999 && uid != 0 {
			continue
		}
		u := LinuxSysUser{}
		u.Uid = uid
		gid, err := strconv.Atoi(fields[3])
		if err != nil {
			continue
		}
		u.Gid = gid
		u.Name = fields[0]
		u.Shell = fields[6]
		u.Home = fields[5]
		s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
		if err == nil {
			u.SSHkeyInfo = s
		}
		result[u.Uid] = u
	}
	return result
}

type OnlineUser struct {
	Name      string    `json:"name"`
	Type      string    `json:"type"`
	When      time.Time `json:"loginTime"`
	Pid       int       `json:"pid"`
	LoginFrom string    `json:"loginForm"`
}

func (ou OnlineUser) String() string {
	return fmt.Sprintf("name:%s type:%s when:%s pids:%v login from:%s", ou.Name, ou.Type, ou.When.Format("2006-01-02 15:04"), ou.Pid, ou.LoginFrom)
}

func (ou OnlineUser) Sha256sum() [32]byte {
	return sha256.Sum256([]byte(ou.String()))
}

var (
	ReOnLineUser    = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+([a-zA-Z0-9/]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+(?:old|\.|\d{2}:\d{2})\s+(\d+)\s+\((.*)\)$`) // sshd远程登录的
	ReOnLineUserTTY = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+(tty[0-9]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+(?:old|\.|\d{2}:\d{2})\s+(\d*(?:,\d*)*)$`)       // 通过控制台登录的
	ReOnLineUserX   = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+(:[0-9]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+\?\s+(\d*(?:,\d*)*).*$`)                           // 通过图像界面
)

// GetOnlineUser
func GetOnlineUser() ([]OnlineUser, error) {
	output, err := exec.Command("who", "-u").Output()
	if err != nil {
		return nil, err
	}
	lines := strings.Split(strings.Trim(string(output), "\n"), "\n")
	result := make([]OnlineUser, 0, 16)
	for _, line := range lines {
		if ReOnLineUser.MatchString(line) {
			m := ReOnLineUser.FindStringSubmatch(line)
			if len(m) != 6 {
				continue
			}
			u := OnlineUser{}
			u.Name = m[1]
			u.Type = m[2]
			t, err := time.Parse("2006-01-02 15:04", m[3])
			if err != nil {
				return nil, err
			}
			u.When = t
			u.LoginFrom = m[5]
			p, err := strconv.Atoi(m[4])
			if err != nil {
				return nil, err
			}
			u.Pid = p
			result = append(result, u)
		}
	}
	return result, nil
}

var (
	ReNum = regexp.MustCompile(`^\d+$`)
)

func GetPidChild(pid int) ([]int, error) {
	items, err := os.ReadDir("/proc")
	if err != nil {
		return nil, err
	}
	result := make([]int, 0, 8)
	for _, item := range items {
		if !item.IsDir() {
			continue
		}
		if !ReNum.MatchString(item.Name()) {
			continue
		}
		ppid, err := getPPID(item.Name())
		if err != nil {
			continue
		}
		if ppid == pid {
			i, err := strconv.Atoi(item.Name())
			if err == nil {
				result = append(result, i)
			}
		}
	}
	slices.Sort(result)
	return result, nil
}

// GetPidChild2 并发获取进程的子进程，并发数为8
func GetPidChild2(pid int) ([]int, error) {
	items, err := os.ReadDir("/proc")
	if err != nil {
		return nil, err
	}

	result := make([]int, 0, 16)
	resultLock := sync.Mutex{}

	// 指定并发数为8
	goroutineNum := 8
	wg := sync.WaitGroup{}
	wg.Add(goroutineNum)

	// 创建管道，并为每个管道添加一个处理goroutine
	cs := make([]chan string, 0, goroutineNum)
	for range goroutineNum {
		c := make(chan string, 128)
		cs = append(cs, c)
		go func(sc <-chan string, ppid int, wg *sync.WaitGroup) {
			for pid := range sc {
				if pid == "0" {
					break
				}
				p, err := getPPID(pid)
				if err == nil && p == ppid {
					pp, err := strconv.Atoi(pid)
					if err == nil {
						resultLock.Lock()
						result = append(result, pp)
						resultLock.Unlock()
					}
				}
			}
			wg.Done()
		}(c, pid, &wg)
	}
	// 向goroutine分发任务
	for i, item := range items {
		cs[i%goroutineNum] <- item.Name()
	}
	// 向goroutine发送关闭信号
	for _, c := range cs {
		c <- "0"
	}
	// 等待goroutine关闭
	wg.Wait()
	// 关闭管道
	for _, c := range cs {
		close(c)
	}
	slices.Sort(result)
	return result, nil
}

func getPPID(pid string) (int, error) {
	path := fmt.Sprintf("/proc/%s/status", pid)
	content, err := os.ReadFile(path)
	if err != nil {
		return 0, err
	}
	lines := strings.Split(string(content), "\n")
	for _, line := range lines {
		if !strings.HasPrefix(line, "PPid:") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) != 2 {
			continue
		}
		return strconv.Atoi(fields[1])
	}
	return 0, fmt.Errorf("error not find PPid in %s", path)
}
